forked from Karylab-cklius/vllm
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6db7065e6 | ||
|
|
a2bedb84b6 | ||
|
|
21d8aa7ecc | ||
|
|
4288f3d3a5 | ||
|
|
97cbc212b4 |
@@ -1,11 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -euox pipefail
|
||||
|
||||
export VLLM_CPU_KVCACHE_SPACE=1
|
||||
export VLLM_CPU_KVCACHE_SPACE=1
|
||||
export VLLM_CPU_CI_ENV=1
|
||||
# Skip torch.compile via vLLM's --enforce-eager flag (passed below) instead of
|
||||
# TORCH_COMPILE_DISABLE=1, which torch 2.12 no longer treats as a silent no-op
|
||||
# when callers specify fullgraph=True.
|
||||
# Reduce sub-processes for acceleration
|
||||
export TORCH_COMPILE_DISABLE=1
|
||||
export VLLM_ENABLE_V1_MULTIPROCESSING=0
|
||||
|
||||
SDE_ARCHIVE="sde-external-10.7.0-2026-02-18-lin.tar.xz"
|
||||
@@ -50,15 +49,15 @@ wait_for_pid_and_check_log() {
|
||||
}
|
||||
|
||||
# Test Sky Lake (AVX512F)
|
||||
./sde/sde64 -skl -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 --enforce-eager > test_0.log 2>&1 &
|
||||
./sde/sde64 -skl -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 > test_0.log 2>&1 &
|
||||
PID_TEST_0=$!
|
||||
|
||||
# Test Cascade Lake (AVX512F + VNNI)
|
||||
./sde/sde64 -clx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 --enforce-eager > test_1.log 2>&1 &
|
||||
./sde/sde64 -clx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 > test_1.log 2>&1 &
|
||||
PID_TEST_1=$!
|
||||
|
||||
# Test Cooper Lake (AVX512F + VNNI + BF16)
|
||||
./sde/sde64 -cpx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 --enforce-eager > test_2.log 2>&1 &
|
||||
./sde/sde64 -cpx -- python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --dtype bfloat16 > test_2.log 2>&1 &
|
||||
PID_TEST_2=$!
|
||||
|
||||
wait_for_pid_and_check_log $PID_TEST_0 test_0.log
|
||||
|
||||
@@ -5,7 +5,7 @@ steps:
|
||||
- label: PyTorch Compilation Unit Tests
|
||||
device: h200_35gb
|
||||
key: pytorch-compilation-unit-tests
|
||||
timeout_in_minutes: 150
|
||||
timeout_in_minutes: 110
|
||||
source_file_dependencies:
|
||||
- vllm/__init__.py
|
||||
- vllm/_aiter_ops.py
|
||||
|
||||
+2
-2
@@ -68,8 +68,8 @@ endif()
|
||||
# requirements.txt files and should be kept consistent. The ROCm torch
|
||||
# versions are derived from docker/Dockerfile.rocm
|
||||
#
|
||||
set(TORCH_SUPPORTED_VERSION_CUDA "2.13.0")
|
||||
set(TORCH_SUPPORTED_VERSION_ROCM "2.13.0")
|
||||
set(TORCH_SUPPORTED_VERSION_CUDA "2.11.0")
|
||||
set(TORCH_SUPPORTED_VERSION_ROCM "2.11.0")
|
||||
# TORCH_NIGHTLY=1 builds run against unpinned nightly wheels, so the supported-
|
||||
# version check would always warn. Only treat it as a nightly build when the
|
||||
# value is exactly "1" (the bootstrap exports TORCH_NIGHTLY=0 by default, which
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
# docker buildx bake -f docker/docker-bake.hcl -f docker/versions.json
|
||||
# =============================================================================
|
||||
|
||||
ARG CUDA_VERSION=13.0.3
|
||||
ARG CUDA_VERSION=13.0.2
|
||||
ARG PYTHON_VERSION=3.12
|
||||
ARG UBUNTU_VERSION=22.04
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"_comment": "Auto-generated from Dockerfile ARGs. Do not edit manually. Run: python tools/generate_versions_json.py",
|
||||
"variable": {
|
||||
"CUDA_VERSION": {
|
||||
"default": "13.0.3"
|
||||
"default": "13.0.2"
|
||||
},
|
||||
"PYTHON_VERSION": {
|
||||
"default": "3.12"
|
||||
@@ -11,10 +11,10 @@
|
||||
"default": "22.04"
|
||||
},
|
||||
"BUILD_BASE_IMAGE": {
|
||||
"default": "nvidia/cuda:13.0.3-devel-ubuntu22.04"
|
||||
"default": "nvidia/cuda:13.0.2-devel-ubuntu22.04"
|
||||
},
|
||||
"FINAL_BASE_IMAGE": {
|
||||
"default": "nvidia/cuda:13.0.3-base-ubuntu22.04"
|
||||
"default": "nvidia/cuda:13.0.2-base-ubuntu22.04"
|
||||
},
|
||||
"BUILD_OS": {
|
||||
"default": "ubuntu"
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ requires = [
|
||||
"setuptools>=77.0.3,<81.0.0",
|
||||
"setuptools-scm>=8.0",
|
||||
"setuptools-rust>=1.9.0",
|
||||
"torch == 2.13.0",
|
||||
"torch == 2.11.0",
|
||||
"wheel",
|
||||
"jinja2",
|
||||
]
|
||||
|
||||
@@ -4,8 +4,8 @@ packaging>=24.2
|
||||
setuptools==77.0.3 # this version can reuse CMake build dir
|
||||
setuptools-scm>=8
|
||||
setuptools-rust>=1.9.0
|
||||
torch==2.13.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
|
||||
torch==2.13.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
|
||||
torch==2.11.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
|
||||
torch==2.11.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
|
||||
wheel
|
||||
jinja2>=3.1.6
|
||||
regex
|
||||
|
||||
@@ -5,7 +5,7 @@ packaging>=24.2
|
||||
setuptools>=77.0.3,<81.0.0
|
||||
setuptools-scm>=8
|
||||
setuptools-rust>=1.9.0
|
||||
torch==2.13.0
|
||||
torch==2.11.0
|
||||
wheel
|
||||
jinja2>=3.1.6
|
||||
regex
|
||||
|
||||
@@ -6,8 +6,8 @@ setuptools==77.0.3 # this version can reuse CMake build dir
|
||||
numba == 0.65.0; platform_machine != "s390x" # Required for N-gram speculative decoding
|
||||
|
||||
# Dependencies for CPUs
|
||||
torch==2.13.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
|
||||
torch==2.13.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
|
||||
torch==2.11.0+cpu; platform_machine == "x86_64" or platform_machine == "s390x" or platform_machine == "aarch64"
|
||||
torch==2.11.0; platform_system == "Darwin" or platform_machine == "ppc64le" or platform_machine == "riscv64"
|
||||
|
||||
# required for the image processor of minicpm-o-2_6, this must be updated alongside torch
|
||||
torchaudio; platform_machine != "s390x" and platform_machine != "riscv64"
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
numba == 0.65.0 # Required for N-gram speculative decoding
|
||||
|
||||
# Dependencies for NVIDIA GPUs
|
||||
torch==2.13.0
|
||||
torch==2.11.0
|
||||
torchaudio==2.11.0
|
||||
# These must be updated alongside torch
|
||||
torchvision==0.28.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version
|
||||
torchvision==0.26.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version
|
||||
torchcodec >= 0.14
|
||||
PyNvVideoCodec==2.0.4
|
||||
# FlashInfer should be updated together with the Dockerfile
|
||||
|
||||
@@ -1107,7 +1107,7 @@ tokenizers==0.22.2
|
||||
# -r requirements/test/../common.txt
|
||||
# -r requirements/test/cuda.in
|
||||
# transformers
|
||||
torch==2.13.0+cpu
|
||||
torch==2.11.0+cpu
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# accelerate
|
||||
@@ -1134,7 +1134,7 @@ torchaudio==2.11.0+cpu
|
||||
# vocos
|
||||
torchcodec==0.14.0+cpu
|
||||
# via -r requirements/test/cuda.in
|
||||
torchvision==0.28.0+cpu
|
||||
torchvision==0.26.0+cpu
|
||||
# via
|
||||
# -r requirements/test/cuda.in
|
||||
# open-clip-torch
|
||||
|
||||
@@ -28,9 +28,9 @@ soundfile # required for audio tests
|
||||
jiwer # required for audio tests
|
||||
tblib # for pickling test exceptions
|
||||
timm >=1.0.17 # required for internvl and gemma3n-mm test
|
||||
torch==2.13.0
|
||||
torch==2.11.0
|
||||
torchaudio==2.11.0
|
||||
torchvision==0.28.0
|
||||
torchvision==0.26.0
|
||||
transformers_stream_generator # required for qwen-vl test
|
||||
matplotlib # required for qwen-vl test
|
||||
mistral_common[image,audio] >= 1.11.5 # required for voxtral test
|
||||
|
||||
@@ -159,7 +159,7 @@ cuda-bindings==13.0.3
|
||||
# via torch
|
||||
cuda-pathfinder==1.3.3
|
||||
# via cuda-bindings
|
||||
cuda-toolkit==13.0.3.0
|
||||
cuda-toolkit==13.0.2
|
||||
# via torch
|
||||
cupy-cuda12x==13.6.0
|
||||
# via ray
|
||||
@@ -599,7 +599,7 @@ numpy==2.2.6
|
||||
# tritonclient
|
||||
# vocos
|
||||
# xgrammar
|
||||
nvidia-cublas==13.1.1.3
|
||||
nvidia-cublas==13.1.0.3
|
||||
# via
|
||||
# cuda-toolkit
|
||||
# nvidia-cudnn-cu13
|
||||
@@ -607,12 +607,10 @@ nvidia-cublas==13.1.1.3
|
||||
nvidia-cuda-cupti==13.0.85
|
||||
# via cuda-toolkit
|
||||
nvidia-cuda-nvrtc==13.0.88
|
||||
# via
|
||||
# cuda-toolkit
|
||||
# nvidia-cublas
|
||||
# via cuda-toolkit
|
||||
nvidia-cuda-runtime==13.0.96
|
||||
# via cuda-toolkit
|
||||
nvidia-cudnn-cu13==9.20.0.48
|
||||
nvidia-cudnn-cu13==9.19.0.56
|
||||
# via torch
|
||||
nvidia-cufft==12.0.0.61
|
||||
# via cuda-toolkit
|
||||
@@ -626,9 +624,9 @@ nvidia-cusparse==12.6.3.3
|
||||
# via
|
||||
# cuda-toolkit
|
||||
# nvidia-cusolver
|
||||
nvidia-cusparselt-cu13==0.8.1
|
||||
nvidia-cusparselt-cu13==0.8.0
|
||||
# via torch
|
||||
nvidia-nccl-cu13==2.29.7
|
||||
nvidia-nccl-cu13==2.28.9
|
||||
# via torch
|
||||
nvidia-nvjitlink==13.0.88
|
||||
# via
|
||||
@@ -1204,7 +1202,7 @@ tokenizers==0.22.2
|
||||
# -r requirements/test/../common.txt
|
||||
# -r requirements/test/cuda.in
|
||||
# transformers
|
||||
torch==2.13.0+cu130
|
||||
torch==2.11.0+cu130
|
||||
# via
|
||||
# -c requirements/cuda.txt
|
||||
# -r requirements/test/cuda.in
|
||||
@@ -1235,7 +1233,7 @@ torchcodec==0.14.0+cu130
|
||||
# via
|
||||
# -c requirements/cuda.txt
|
||||
# -r requirements/test/cuda.in
|
||||
torchvision==0.28.0+cu130
|
||||
torchvision==0.26.0+cu130
|
||||
# via
|
||||
# -c requirements/cuda.txt
|
||||
# -r requirements/test/cuda.in
|
||||
@@ -1272,7 +1270,7 @@ transformers==5.13.1
|
||||
# xgrammar
|
||||
transformers-stream-generator==0.0.5
|
||||
# via -r requirements/test/cuda.in
|
||||
triton==3.7.1
|
||||
triton==3.6.0
|
||||
# via
|
||||
# torch
|
||||
# xgrammar
|
||||
|
||||
Generated
+40
-20
@@ -4252,18 +4252,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subenum"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3d08fe7078c57309d5c3d938e50eba95ba1d33b9c3a101a8465fc6861a5416"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -5516,15 +5504,11 @@ dependencies = [
|
||||
"asynk-strim-attr",
|
||||
"bytes",
|
||||
"clap",
|
||||
"easy-ext",
|
||||
"expect-test",
|
||||
"futures",
|
||||
"half",
|
||||
"indexmap 2.13.0",
|
||||
"itertools 0.14.0",
|
||||
"llm-multimodal",
|
||||
"minijinja",
|
||||
"minijinja-contrib",
|
||||
"ndarray 0.17.2",
|
||||
"oss-harmony",
|
||||
"paste",
|
||||
@@ -5535,17 +5519,16 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"serial_test",
|
||||
"strum",
|
||||
"subenum",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror-ext",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"trait-set",
|
||||
"uuid",
|
||||
"vllm-chat-renderer",
|
||||
"vllm-chat-types",
|
||||
"vllm-engine-core-client",
|
||||
"vllm-llm",
|
||||
"vllm-parser",
|
||||
@@ -5555,6 +5538,42 @@ dependencies = [
|
||||
"zeromq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vllm-chat-renderer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"enum-as-inner",
|
||||
"expect-test",
|
||||
"indexmap 2.13.0",
|
||||
"itertools 0.14.0",
|
||||
"minijinja",
|
||||
"minijinja-contrib",
|
||||
"oss-harmony",
|
||||
"serde",
|
||||
"serde-json-fmt",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror-ext",
|
||||
"time",
|
||||
"tracing",
|
||||
"vllm-chat-types",
|
||||
"vllm-tokenizer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vllm-chat-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"easy-ext",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vllm-cmd"
|
||||
version = "0.1.0"
|
||||
@@ -5695,11 +5714,11 @@ dependencies = [
|
||||
"expect-test",
|
||||
"futures",
|
||||
"openai-protocol",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror-ext",
|
||||
"tool-parser",
|
||||
"vllm-chat-types",
|
||||
"vllm-tokenizer",
|
||||
"winnow",
|
||||
"xgrammar-structural-tag",
|
||||
@@ -5806,6 +5825,7 @@ dependencies = [
|
||||
"rustc-hash 1.1.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"tekken-rs",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
members = [
|
||||
"src/bench",
|
||||
"src/chat",
|
||||
"src/chat-renderer",
|
||||
"src/chat-types",
|
||||
"src/cmd",
|
||||
"src/engine-core-client",
|
||||
"src/llm",
|
||||
@@ -135,6 +137,8 @@ uuid = { version = "1.22.0", features = ["v4"] }
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
vllm-bench = { path = "src/bench" }
|
||||
vllm-chat = { path = "src/chat" }
|
||||
vllm-chat-renderer = { path = "src/chat-renderer" }
|
||||
vllm-chat-types = { path = "src/chat-types" }
|
||||
vllm-engine-core-client = { path = "src/engine-core-client" }
|
||||
vllm-llm = { path = "src/llm" }
|
||||
vllm-managed-engine = { path = "src/managed-engine" }
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "vllm-chat-renderer"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
enum-as-inner.workspace = true
|
||||
indexmap.workspace = true
|
||||
itertools.workspace = true
|
||||
minijinja.workspace = true
|
||||
minijinja-contrib.workspace = true
|
||||
openai-harmony.workspace = true
|
||||
serde.workspace = true
|
||||
serde-json-fmt.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
thiserror-ext.workspace = true
|
||||
time.workspace = true
|
||||
tracing.workspace = true
|
||||
vllm-chat-types.workspace = true
|
||||
vllm-tokenizer.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
expect-test.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
+14
-13
@@ -11,8 +11,10 @@ use serde_json::Value;
|
||||
use serde_json_fmt::JsonFormat;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::request::{ChatContent, ChatMessage, ChatRequest, ChatRole, ChatTool};
|
||||
use crate::{AssistantContentBlock, AssistantMessageExt, AssistantToolCall};
|
||||
use crate::{
|
||||
AssistantContentBlock, AssistantMessageExt, AssistantToolCall, ChatContent, ChatMessage,
|
||||
ChatRole, RenderRequest, Tool,
|
||||
};
|
||||
|
||||
const BOS_TOKEN: &str = "<|begin▁of▁sentence|>";
|
||||
const EOS_TOKEN: &str = "<|end▁of▁sentence|>";
|
||||
@@ -39,7 +41,7 @@ struct RenderedToolSchema<'a> {
|
||||
}
|
||||
|
||||
/// Render one chat request into the final prompt string.
|
||||
pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
|
||||
pub(super) fn render_request(request: &RenderRequest<'_>) -> Result<String> {
|
||||
let thinking_mode = match request.enable_thinking()?.unwrap_or(false) {
|
||||
true => ThinkingMode::Thinking,
|
||||
false => ThinkingMode::Chat,
|
||||
@@ -49,20 +51,19 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
|
||||
Some(ChatRole::User | ChatRole::Developer)
|
||||
);
|
||||
let render_offset = isize::from(request.tool_parsing_enabled());
|
||||
let last_user_render_index =
|
||||
find_last_user_render_index(request.messages.as_slice(), render_offset);
|
||||
let last_user_actual_index = find_last_user_actual_index(request.messages.as_slice());
|
||||
let last_user_render_index = find_last_user_render_index(request.messages, render_offset);
|
||||
let last_user_actual_index = find_last_user_actual_index(request.messages);
|
||||
let continue_final_message = request.chat_options.continue_final_message();
|
||||
let mut prompt = String::from(BOS_TOKEN);
|
||||
|
||||
if request.tool_parsing_enabled() {
|
||||
render_system_message(&mut prompt, None, &request.tools)?;
|
||||
render_system_message(&mut prompt, None, request.tools)?;
|
||||
}
|
||||
|
||||
for (message_index, message) in request.messages.iter().enumerate() {
|
||||
render_message(
|
||||
&mut prompt,
|
||||
request.messages.as_slice(),
|
||||
request.messages,
|
||||
message_index,
|
||||
message,
|
||||
render_offset,
|
||||
@@ -168,7 +169,7 @@ fn find_last_user_actual_index(messages: &[ChatMessage]) -> usize {
|
||||
fn render_system_message(
|
||||
out: &mut String,
|
||||
content: Option<&ChatContent>,
|
||||
tools: &[ChatTool],
|
||||
tools: &[Tool],
|
||||
) -> Result<()> {
|
||||
if let Some(content) = content {
|
||||
write_chat_content(out, content)?;
|
||||
@@ -185,7 +186,7 @@ fn render_system_message(
|
||||
fn render_developer_message(
|
||||
out: &mut String,
|
||||
content: &ChatContent,
|
||||
tools: &[ChatTool],
|
||||
tools: &[Tool],
|
||||
opens_thinking: bool,
|
||||
) -> Result<()> {
|
||||
if content.is_empty() {
|
||||
@@ -452,7 +453,7 @@ fn encode_arguments_to_dsml(out: &mut String, tool_call: &AssistantToolCall) ->
|
||||
}
|
||||
|
||||
/// Render the full tool preamble shown to the model.
|
||||
fn render_tools(out: &mut String, tools: &[ChatTool]) -> Result<()> {
|
||||
fn render_tools(out: &mut String, tools: &[Tool]) -> Result<()> {
|
||||
out.push_str(
|
||||
r#"## Tools
|
||||
|
||||
@@ -500,7 +501,7 @@ Here are the functions available in JSONSchema format:
|
||||
|
||||
/// Serialize one typed tool schema into the JSON shape embedded inside
|
||||
/// `<functions>`.
|
||||
fn render_tool_schema(out: &mut String, tool: &ChatTool) -> Result<()> {
|
||||
fn render_tool_schema(out: &mut String, tool: &Tool) -> Result<()> {
|
||||
out.push_str(&json_dumps(&RenderedToolSchema {
|
||||
name: &tool.name,
|
||||
description: tool.description.as_deref(),
|
||||
@@ -517,7 +518,7 @@ fn write_chat_content(out: &mut String, content: &ChatContent) -> Result<()> {
|
||||
ChatContent::Text(text) => out.push_str(text),
|
||||
ChatContent::Parts(parts) => {
|
||||
for part in parts {
|
||||
out.push_str(part.as_text()?);
|
||||
out.push_str(part.as_text().map_err(Error::UnsupportedMultimodalContent)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -3,11 +3,10 @@
|
||||
|
||||
mod encoding;
|
||||
|
||||
use vllm_text::Prompt;
|
||||
|
||||
use super::{ChatRenderer, RenderedPrompt, request_template_kwargs};
|
||||
use super::{
|
||||
ChatRenderer, RenderRequest, RenderedPrompt, RenderedPromptContent, request_template_kwargs,
|
||||
};
|
||||
use crate::Result;
|
||||
use crate::request::ChatRequest;
|
||||
|
||||
/// Dedicated DeepSeek V3.2 renderer.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
@@ -21,12 +20,12 @@ impl DeepSeekV32ChatRenderer {
|
||||
}
|
||||
|
||||
impl ChatRenderer for DeepSeekV32ChatRenderer {
|
||||
fn render(&self, request: &ChatRequest) -> Result<RenderedPrompt> {
|
||||
fn render(&self, request: RenderRequest<'_>) -> Result<RenderedPrompt> {
|
||||
request.validate()?;
|
||||
|
||||
Ok(RenderedPrompt {
|
||||
prompt: Prompt::Text(encoding::render_request(request)?),
|
||||
effective_template_kwargs: request_template_kwargs(request),
|
||||
content: RenderedPromptContent::Text(encoding::render_request(&request)?),
|
||||
effective_template_kwargs: request_template_kwargs(&request),
|
||||
})
|
||||
}
|
||||
}
|
||||
+23
-25
@@ -9,36 +9,35 @@ use thiserror_ext::AsReport;
|
||||
|
||||
use super::DeepSeekV32ChatRenderer;
|
||||
use crate::error::Error;
|
||||
use crate::event::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
use crate::request::{
|
||||
ChatContentPart, ChatMessage, ChatRequest, ChatTool, ChatToolChoice, GenerationPromptMode,
|
||||
use crate::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
use crate::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::{
|
||||
ChatContentPart, ChatMessage, ChatToolChoice, GenerationPromptMode, TestRenderRequest, Tool,
|
||||
};
|
||||
use crate::{ChatRenderer, ChatRole};
|
||||
|
||||
fn render_request(request: &ChatRequest) -> String {
|
||||
fn render_request(request: &TestRenderRequest) -> String {
|
||||
DeepSeekV32ChatRenderer::new()
|
||||
.render(request)
|
||||
.render(request.as_request())
|
||||
.unwrap()
|
||||
.prompt
|
||||
.content
|
||||
.into_text()
|
||||
.expect("deepseek renderer should return text prompt")
|
||||
}
|
||||
|
||||
fn render_result(request: &ChatRequest) -> Result<String, Error> {
|
||||
DeepSeekV32ChatRenderer::new().render(request).map(|rendered| {
|
||||
fn render_result(request: &TestRenderRequest) -> Result<String, Error> {
|
||||
DeepSeekV32ChatRenderer::new().render(request.as_request()).map(|rendered| {
|
||||
rendered
|
||||
.prompt
|
||||
.content
|
||||
.into_text()
|
||||
.expect("deepseek renderer should return text prompt")
|
||||
})
|
||||
}
|
||||
|
||||
fn thinking_request(messages: Vec<ChatMessage>) -> ChatRequest {
|
||||
let mut request = ChatRequest {
|
||||
request_id: "deepseek-v32-small-test".to_string(),
|
||||
fn thinking_request(messages: Vec<ChatMessage>) -> TestRenderRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages,
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
if matches!(
|
||||
request.messages.last().map(ChatMessage::role),
|
||||
@@ -53,7 +52,7 @@ fn thinking_request(messages: Vec<ChatMessage>) -> ChatRequest {
|
||||
request
|
||||
}
|
||||
|
||||
fn fixture_request(input_name: &str) -> ChatRequest {
|
||||
fn fixture_request(input_name: &str) -> TestRenderRequest {
|
||||
fixture_chat_request(&fixture_path(input_name), deepseek_fixture_options())
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ fn deepseek_fixture_options() -> FixtureRequestOptions {
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/renderer/deepseek_v32")
|
||||
.join("src/deepseek_v32")
|
||||
.join("fixtures")
|
||||
.join(name)
|
||||
}
|
||||
@@ -103,13 +102,12 @@ fn renders_official_search_fixture_with_date() {
|
||||
|
||||
#[test]
|
||||
fn request_level_tools_are_lowered_as_synthetic_leading_system_message() {
|
||||
let mut request = ChatRequest {
|
||||
request_id: "deepseek-v32-tools".to_string(),
|
||||
let mut request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::system("System prompt."),
|
||||
ChatMessage::text(ChatRole::User, "Hello"),
|
||||
],
|
||||
tools: vec![ChatTool {
|
||||
tools: vec![Tool {
|
||||
name: "lookup".to_string(),
|
||||
description: Some("Look things up".to_string()),
|
||||
parameters: json!({
|
||||
@@ -124,7 +122,7 @@ fn request_level_tools_are_lowered_as_synthetic_leading_system_message() {
|
||||
strict: None,
|
||||
}],
|
||||
tool_choice: ChatToolChoice::Auto,
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
request
|
||||
.chat_options
|
||||
@@ -272,12 +270,12 @@ fn assistant_after_last_user_requires_reasoning_or_tool_calls() {
|
||||
|
||||
#[test]
|
||||
fn continue_final_assistant_omits_final_eos() {
|
||||
let mut request = ChatRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::user("write"),
|
||||
ChatMessage::assistant_text("partial answer"),
|
||||
],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
request.chat_options.generation_prompt_mode = GenerationPromptMode::ContinueFinalAssistant;
|
||||
|
||||
@@ -289,14 +287,14 @@ fn continue_final_assistant_omits_final_eos() {
|
||||
|
||||
#[test]
|
||||
fn render_rejects_multimodal_input() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::user(vec![ChatContentPart::image_url(
|
||||
"data:image/png;base64,test",
|
||||
)])],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let error = DeepSeekV32ChatRenderer::new().render(&request).unwrap_err();
|
||||
let error = DeepSeekV32ChatRenderer::new().render(request.as_request()).unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
+19
-21
@@ -14,8 +14,10 @@ use serde_json::Value;
|
||||
use serde_json_fmt::JsonFormat;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::request::{ChatContent, ChatMessage, ChatRequest, ChatTool, ReasoningEffort};
|
||||
use crate::{AssistantContentBlock, AssistantMessageExt, AssistantToolCall};
|
||||
use crate::{
|
||||
AssistantContentBlock, AssistantMessageExt, AssistantToolCall, ChatContent, ChatMessage,
|
||||
ReasoningEffort, RenderRequest, Tool,
|
||||
};
|
||||
|
||||
const BOS_TOKEN: &str = "<|begin▁of▁sentence|>";
|
||||
const EOS_TOKEN: &str = "<|end▁of▁sentence|>";
|
||||
@@ -46,14 +48,14 @@ struct RenderedToolSchema<'a> {
|
||||
}
|
||||
|
||||
/// Render one chat request into the final prompt string.
|
||||
pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
|
||||
pub(super) fn render_request(request: &RenderRequest<'_>) -> Result<String> {
|
||||
let (thinking_mode, max_reasoning_effort) = resolve_thinking_options(request)?;
|
||||
let request_tools = request_tools(request);
|
||||
let synthetic_tool_system = needs_synthetic_tool_system(request, request_tools);
|
||||
let drop_thinking = request.parse_template_bool("drop_thinking")?.unwrap_or(true)
|
||||
&& !rendered_tools_present(request, request_tools);
|
||||
let last_user_render_index =
|
||||
find_last_user_render_index(request.messages.as_slice(), synthetic_tool_system);
|
||||
find_last_user_render_index(request.messages, synthetic_tool_system);
|
||||
let mut out = String::from(BOS_TOKEN);
|
||||
if thinking_mode == ThinkingMode::Thinking && max_reasoning_effort {
|
||||
out.push_str(REASONING_EFFORT_MAX);
|
||||
@@ -68,7 +70,7 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
|
||||
}
|
||||
|
||||
for (message_index, message) in request.messages.iter().enumerate() {
|
||||
if is_following_tool_response(request.messages.as_slice(), message_index) {
|
||||
if is_following_tool_response(request.messages, message_index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -101,12 +103,12 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
|
||||
render_assistant_message(&mut out, emit_thinking_block, append_eos, content)?;
|
||||
}
|
||||
ChatMessage::ToolResponse { .. } => {
|
||||
render_tool_response_block(&mut out, request.messages.as_slice(), message_index)?;
|
||||
render_tool_response_block(&mut out, request.messages, message_index)?;
|
||||
}
|
||||
}
|
||||
|
||||
if is_user_like_entry(message)
|
||||
&& next_rendered_entry_is_assistant_or_end(request.messages.as_slice(), message_index)
|
||||
&& next_rendered_entry_is_assistant_or_end(request.messages, message_index)
|
||||
{
|
||||
write_assistant_transition(
|
||||
&mut out,
|
||||
@@ -124,7 +126,7 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
|
||||
/// wrapper, the Rust renderer only consumes the typed top-level
|
||||
/// `reasoning_effort`; the generic template-kwargs map is left for HF
|
||||
/// templates.
|
||||
fn resolve_thinking_options(request: &ChatRequest) -> Result<(ThinkingMode, bool)> {
|
||||
fn resolve_thinking_options(request: &RenderRequest<'_>) -> Result<(ThinkingMode, bool)> {
|
||||
let mut thinking_mode = match request.enable_thinking()?.unwrap_or(false) {
|
||||
true => ThinkingMode::Thinking,
|
||||
false => ThinkingMode::Chat,
|
||||
@@ -141,16 +143,16 @@ fn resolve_thinking_options(request: &ChatRequest) -> Result<(ThinkingMode, bool
|
||||
}
|
||||
|
||||
/// Return request-level tools only when native tool parsing is enabled.
|
||||
fn request_tools(request: &ChatRequest) -> &[ChatTool] {
|
||||
fn request_tools<'a>(request: &RenderRequest<'a>) -> &'a [Tool] {
|
||||
if request.tool_parsing_enabled() {
|
||||
request.tools.as_slice()
|
||||
request.tools
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
/// Return whether request tools need a synthetic leading system entry.
|
||||
fn needs_synthetic_tool_system(request: &ChatRequest, request_tools: &[ChatTool]) -> bool {
|
||||
fn needs_synthetic_tool_system(request: &RenderRequest<'_>, request_tools: &[Tool]) -> bool {
|
||||
!request_tools.is_empty()
|
||||
&& !request
|
||||
.messages
|
||||
@@ -159,7 +161,7 @@ fn needs_synthetic_tool_system(request: &ChatRequest, request_tools: &[ChatTool]
|
||||
}
|
||||
|
||||
/// Return whether any rendered message carries tool schemas.
|
||||
fn rendered_tools_present(request: &ChatRequest, request_tools: &[ChatTool]) -> bool {
|
||||
fn rendered_tools_present(request: &RenderRequest<'_>, request_tools: &[Tool]) -> bool {
|
||||
!request_tools.is_empty()
|
||||
|| request.messages.iter().any(|message| {
|
||||
matches!(
|
||||
@@ -228,7 +230,7 @@ fn next_rendered_entry_is_assistant_or_end(messages: &[ChatMessage], message_ind
|
||||
}
|
||||
|
||||
/// Render the tool preamble shown to the model, V4 flavor.
|
||||
fn render_tools(out: &mut String, tools: &[ChatTool]) -> Result<()> {
|
||||
fn render_tools(out: &mut String, tools: &[Tool]) -> Result<()> {
|
||||
out.push_str(
|
||||
r#"## Tools
|
||||
|
||||
@@ -269,7 +271,7 @@ Otherwise, output directly after </think> with tool calls or final response.
|
||||
}
|
||||
|
||||
/// Serialize one typed tool schema into the JSON shape embedded in the prompt.
|
||||
fn render_tool_schema(out: &mut String, tool: &ChatTool) -> Result<()> {
|
||||
fn render_tool_schema(out: &mut String, tool: &Tool) -> Result<()> {
|
||||
out.push_str(&json_dumps(&RenderedToolSchema {
|
||||
name: &tool.name,
|
||||
description: tool.description.as_deref(),
|
||||
@@ -283,7 +285,7 @@ fn render_tool_schema(out: &mut String, tool: &ChatTool) -> Result<()> {
|
||||
fn render_system_message(
|
||||
out: &mut String,
|
||||
content: Option<&ChatContent>,
|
||||
tools: &[ChatTool],
|
||||
tools: &[Tool],
|
||||
) -> Result<()> {
|
||||
if let Some(content) = content {
|
||||
write_chat_content(out, content)?;
|
||||
@@ -296,11 +298,7 @@ fn render_system_message(
|
||||
}
|
||||
|
||||
/// Developer messages are rendered as user-like turns with optional tools.
|
||||
fn render_developer_message(
|
||||
out: &mut String,
|
||||
content: &ChatContent,
|
||||
tools: &[ChatTool],
|
||||
) -> Result<()> {
|
||||
fn render_developer_message(out: &mut String, content: &ChatContent, tools: &[Tool]) -> Result<()> {
|
||||
if content.is_empty() {
|
||||
return Err(Error::ChatTemplate(
|
||||
"invalid DeepSeek V4 developer message: empty content".to_string(),
|
||||
@@ -519,7 +517,7 @@ fn write_chat_content(out: &mut String, content: &ChatContent) -> Result<()> {
|
||||
ChatContent::Text(text) => out.push_str(text),
|
||||
ChatContent::Parts(parts) => {
|
||||
for part in parts {
|
||||
out.push_str(part.as_text()?);
|
||||
out.push_str(part.as_text().map_err(Error::UnsupportedMultimodalContent)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -3,29 +3,29 @@
|
||||
|
||||
mod encoding;
|
||||
|
||||
use vllm_text::Prompt;
|
||||
|
||||
use super::{ChatRenderer, RenderedPrompt, request_template_kwargs};
|
||||
use super::{
|
||||
ChatRenderer, RenderRequest, RenderedPrompt, RenderedPromptContent, request_template_kwargs,
|
||||
};
|
||||
use crate::Result;
|
||||
use crate::request::ChatRequest;
|
||||
|
||||
/// Dedicated DeepSeek V4 renderer.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct DeepSeekV4ChatRenderer;
|
||||
|
||||
impl DeepSeekV4ChatRenderer {
|
||||
/// Create the dedicated DeepSeek V4 renderer.
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatRenderer for DeepSeekV4ChatRenderer {
|
||||
fn render(&self, request: &ChatRequest) -> Result<RenderedPrompt> {
|
||||
fn render(&self, request: RenderRequest<'_>) -> Result<RenderedPrompt> {
|
||||
request.validate()?;
|
||||
|
||||
Ok(RenderedPrompt {
|
||||
prompt: Prompt::Text(encoding::render_request(request)?),
|
||||
effective_template_kwargs: request_template_kwargs(request),
|
||||
content: RenderedPromptContent::Text(encoding::render_request(&request)?),
|
||||
effective_template_kwargs: request_template_kwargs(&request),
|
||||
})
|
||||
}
|
||||
}
|
||||
+21
-21
@@ -8,20 +8,20 @@ use serde_json::Value;
|
||||
|
||||
use super::DeepSeekV4ChatRenderer;
|
||||
use crate::ChatRenderer;
|
||||
use crate::event::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
use crate::request::{ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort};
|
||||
use crate::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
use crate::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::{ChatMessage, GenerationPromptMode, ReasoningEffort, TestRenderRequest};
|
||||
|
||||
fn render_request(request: &ChatRequest) -> String {
|
||||
fn render_request(request: &TestRenderRequest) -> String {
|
||||
DeepSeekV4ChatRenderer::new()
|
||||
.render(request)
|
||||
.render(request.as_request())
|
||||
.unwrap()
|
||||
.prompt
|
||||
.content
|
||||
.into_text()
|
||||
.expect("deepseek v4 renderer should return text prompt")
|
||||
}
|
||||
|
||||
fn fixture_request(input_name: &str) -> ChatRequest {
|
||||
fn fixture_request(input_name: &str) -> TestRenderRequest {
|
||||
fixture_chat_request(&fixture_path(input_name), deepseek_fixture_options())
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ fn deepseek_fixture_options() -> FixtureRequestOptions {
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/renderer/deepseek_v4")
|
||||
.join("src/deepseek_v4")
|
||||
.join("fixtures")
|
||||
.join(name)
|
||||
}
|
||||
@@ -63,9 +63,9 @@ fn renders_v4_fixture_2_multi_turn_drop_thinking() {
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_max_adds_prefix_when_thinking_is_enabled() {
|
||||
let mut request = ChatRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::user("solve it")],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
request
|
||||
.chat_options
|
||||
@@ -86,9 +86,9 @@ fn reasoning_effort_max_adds_prefix_when_thinking_is_enabled() {
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_none_disables_thinking() {
|
||||
let mut request = ChatRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::user("answer directly")],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
request
|
||||
.chat_options
|
||||
@@ -104,9 +104,9 @@ fn reasoning_effort_none_disables_thinking() {
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_template_kwarg_is_ignored() {
|
||||
let mut request = ChatRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::user("solve it")],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
request
|
||||
.chat_options
|
||||
@@ -124,7 +124,7 @@ fn reasoning_effort_template_kwarg_is_ignored() {
|
||||
|
||||
#[test]
|
||||
fn tool_results_are_sorted_by_previous_assistant_tool_call_order() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::assistant_blocks(vec![
|
||||
AssistantContentBlock::ToolCall(AssistantToolCall {
|
||||
@@ -141,7 +141,7 @@ fn tool_results_are_sorted_by_previous_assistant_tool_call_order() {
|
||||
ChatMessage::tool_response("first result", "first"),
|
||||
ChatMessage::tool_response("second result", "second"),
|
||||
],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let rendered = render_request(&request);
|
||||
@@ -164,7 +164,7 @@ fn tool_results_are_sorted_by_previous_assistant_tool_call_order() {
|
||||
|
||||
#[test]
|
||||
fn drop_thinking_false_keeps_prior_assistant_reasoning() {
|
||||
let mut request = ChatRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::assistant_blocks(vec![
|
||||
AssistantContentBlock::Reasoning {
|
||||
@@ -176,7 +176,7 @@ fn drop_thinking_false_keeps_prior_assistant_reasoning() {
|
||||
]),
|
||||
ChatMessage::user("next"),
|
||||
],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
request
|
||||
.chat_options
|
||||
@@ -197,16 +197,16 @@ fn drop_thinking_false_keeps_prior_assistant_reasoning() {
|
||||
|
||||
#[test]
|
||||
fn continue_final_assistant_omits_final_eos() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::user("write"),
|
||||
ChatMessage::assistant_text("partial answer"),
|
||||
],
|
||||
chat_options: crate::request::ChatOptions {
|
||||
chat_options: crate::ChatOptions {
|
||||
generation_prompt_mode: GenerationPromptMode::ContinueFinalAssistant,
|
||||
..Default::default()
|
||||
},
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let rendered = render_request(&request);
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
type BoxedError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
/// Error returned while constructing or applying a chat renderer.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Rendering requires at least one chat message.
|
||||
#[error("chat request must contain at least one message")]
|
||||
EmptyMessages,
|
||||
/// Continuation mode requires the final message to be an assistant turn.
|
||||
#[error("cannot continue the final message when the last message is not from the assistant")]
|
||||
ContinueFinalAssistantWithoutFinalAssistant,
|
||||
/// The selected renderer requires a chat template and none was configured.
|
||||
#[error("chat template is required but none was configured")]
|
||||
MissingChatTemplate,
|
||||
/// A chat template could not be compiled or applied.
|
||||
#[error("chat template error: {0}")]
|
||||
ChatTemplate(String),
|
||||
/// The selected renderer cannot represent the given multimodal part.
|
||||
#[error("unsupported multimodal content: {0}")]
|
||||
UnsupportedMultimodalContent(&'static str),
|
||||
/// The process-wide GPT-OSS Harmony encoding could not be initialized.
|
||||
#[error("failed to initialize the Harmony encoding")]
|
||||
HarmonyEncoding {
|
||||
/// Underlying Harmony initialization failure.
|
||||
#[source]
|
||||
error: BoxedError,
|
||||
},
|
||||
/// Tokenizer construction or encoding failed.
|
||||
#[error(transparent)]
|
||||
Tokenizer(#[from] vllm_tokenizer::TokenizerError),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Whether this error should be reported as invalid request input when
|
||||
/// raised while rendering.
|
||||
pub fn is_request_validation_error(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::EmptyMessages
|
||||
| Self::ContinueFinalAssistantWithoutFinalAssistant
|
||||
| Self::MissingChatTemplate
|
||||
| Self::ChatTemplate(_)
|
||||
| Self::UnsupportedMultimodalContent(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Result returned by chat renderer operations.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
+2
-2
@@ -12,13 +12,13 @@ use thiserror_ext::AsReport as _;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Lazily load the shared GPT-OSS Harmony encoding once per process.
|
||||
pub(crate) fn harmony_encoding() -> Result<&'static HarmonyEncoding> {
|
||||
pub fn harmony_encoding() -> Result<&'static HarmonyEncoding> {
|
||||
static ENCODING: LazyLock<anyhow::Result<HarmonyEncoding>> = LazyLock::new(|| {
|
||||
load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)
|
||||
.context("failed to load harmony encoding for gpt-oss")
|
||||
});
|
||||
|
||||
ENCODING.as_ref().map_err(|error| Error::HarmonyOutputParsing {
|
||||
ENCODING.as_ref().map_err(|error| Error::HarmonyEncoding {
|
||||
error: error.to_report_string().into(),
|
||||
})
|
||||
}
|
||||
+28
-26
@@ -3,7 +3,7 @@
|
||||
|
||||
//! Native Harmony chat renderer for `gpt_oss`.
|
||||
|
||||
pub(crate) mod encoding;
|
||||
mod encoding;
|
||||
|
||||
use openai_harmony::HarmonyEncoding;
|
||||
use openai_harmony::chat::{
|
||||
@@ -12,14 +12,16 @@ use openai_harmony::chat::{
|
||||
};
|
||||
use thiserror_ext::AsReport as _;
|
||||
use time::macros::format_description;
|
||||
use vllm_text::Prompt;
|
||||
|
||||
use self::encoding::harmony_encoding;
|
||||
use super::{ChatRenderer, RenderedPrompt, request_template_kwargs};
|
||||
pub use self::encoding::harmony_encoding;
|
||||
use super::{
|
||||
ChatRenderer, RenderRequest, RenderedPrompt, RenderedPromptContent, request_template_kwargs,
|
||||
};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::event::AssistantContentBlock;
|
||||
use crate::request::{ChatContent, ChatMessage, ChatRequest, ChatTool, GenerationPromptMode};
|
||||
use crate::{AssistantMessageExt as _, ReasoningEffort};
|
||||
use crate::{
|
||||
AssistantContentBlock, AssistantMessageExt as _, ChatContent, ChatMessage,
|
||||
GenerationPromptMode, ReasoningEffort, Tool,
|
||||
};
|
||||
|
||||
const SYSTEM_START_DATE_ENV: &str = "VLLM_SYSTEM_START_DATE";
|
||||
const HARMONY_SYSTEM_INSTRUCTIONS_ENV: &str = "VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS";
|
||||
@@ -73,7 +75,7 @@ impl HarmonyChatRenderer {
|
||||
///
|
||||
/// Harmony owns both prompt formatting and tokenization, so the Rust
|
||||
/// frontend bypasses the generic HF tokenizer path for GPT-OSS input.
|
||||
fn render_token_ids(&self, request: &ChatRequest) -> Result<Vec<u32>> {
|
||||
fn render_token_ids(&self, request: &RenderRequest<'_>) -> Result<Vec<u32>> {
|
||||
if request.has_multimodal() {
|
||||
return Err(Error::UnsupportedMultimodalContent("image_url"));
|
||||
}
|
||||
@@ -111,12 +113,12 @@ impl HarmonyChatRenderer {
|
||||
}
|
||||
|
||||
impl ChatRenderer for HarmonyChatRenderer {
|
||||
/// Render a chat request as [`Prompt::TokenIds`] with template kwargs echoed
|
||||
/// for downstream accounting/debugging.
|
||||
fn render(&self, request: &ChatRequest) -> Result<RenderedPrompt> {
|
||||
/// Render a chat request as token IDs with template kwargs echoed for
|
||||
/// downstream accounting/debugging.
|
||||
fn render(&self, request: RenderRequest<'_>) -> Result<RenderedPrompt> {
|
||||
Ok(RenderedPrompt {
|
||||
prompt: Prompt::TokenIds(self.render_token_ids(request)?),
|
||||
effective_template_kwargs: request_template_kwargs(request),
|
||||
content: RenderedPromptContent::TokenIds(self.render_token_ids(&request)?),
|
||||
effective_template_kwargs: request_template_kwargs(&request),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -126,10 +128,10 @@ impl ChatRenderer for HarmonyChatRenderer {
|
||||
/// This adds the Harmony system/developer preamble, peels at most one leading
|
||||
/// system/developer instruction message, and then lowers the remaining chat
|
||||
/// history message-by-message.
|
||||
fn to_harmony_messages(request: &ChatRequest, options: &Options) -> Result<Vec<Message>> {
|
||||
fn to_harmony_messages(request: &RenderRequest<'_>, options: &Options) -> Result<Vec<Message>> {
|
||||
let (instructions, leading_developer_tools, remaining_messages) =
|
||||
peel_leading_instructions(&request.messages)?;
|
||||
let tool_call_names = tool_call_names(&request.messages);
|
||||
peel_leading_instructions(request.messages)?;
|
||||
let tool_call_names = tool_call_names(request.messages);
|
||||
let mut messages =
|
||||
build_harmony_preamble(request, instructions, leading_developer_tools, options)?;
|
||||
|
||||
@@ -147,7 +149,7 @@ fn to_harmony_messages(request: &ChatRequest, options: &Options) -> Result<Vec<M
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn peel_leading_instructions(
|
||||
messages: &[ChatMessage],
|
||||
) -> Result<(Option<String>, Option<&[ChatTool]>, &[ChatMessage])> {
|
||||
) -> Result<(Option<String>, Option<&[Tool]>, &[ChatMessage])> {
|
||||
let Some(first) = messages.first() else {
|
||||
return Ok((None, None, messages));
|
||||
};
|
||||
@@ -172,9 +174,9 @@ fn peel_leading_instructions(
|
||||
/// a developer message depending on `use_system_instructions`; request-level and
|
||||
/// leading developer tools are attached to the developer message.
|
||||
fn build_harmony_preamble(
|
||||
request: &ChatRequest,
|
||||
request: &RenderRequest<'_>,
|
||||
instructions: Option<String>,
|
||||
leading_developer_tools: Option<&[ChatTool]>,
|
||||
leading_developer_tools: Option<&[Tool]>,
|
||||
options: &Options,
|
||||
) -> Result<Vec<Message>> {
|
||||
let mut messages = vec![Message::from_role_and_content(
|
||||
@@ -211,12 +213,12 @@ fn build_harmony_preamble(
|
||||
|
||||
/// Collect request-level and leading developer function tools for the preamble.
|
||||
fn preamble_tool_descriptions(
|
||||
request: &ChatRequest,
|
||||
leading_developer_tools: Option<&[ChatTool]>,
|
||||
request: &RenderRequest<'_>,
|
||||
leading_developer_tools: Option<&[Tool]>,
|
||||
) -> Vec<ToolDescription> {
|
||||
let mut tools = Vec::new();
|
||||
if request.tool_parsing_enabled() {
|
||||
tools.extend(to_tool_descriptions(&request.tools));
|
||||
tools.extend(to_tool_descriptions(request.tools));
|
||||
}
|
||||
if let Some(leading_developer_tools) = leading_developer_tools {
|
||||
tools.extend(to_tool_descriptions(leading_developer_tools));
|
||||
@@ -311,7 +313,7 @@ fn to_harmony_message(
|
||||
fn system_or_developer_message(
|
||||
role: &str,
|
||||
instructions: String,
|
||||
tools: Option<&[ChatTool]>,
|
||||
tools: Option<&[Tool]>,
|
||||
options: &Options,
|
||||
) -> Result<Message> {
|
||||
if role == "system" && options.use_system_instructions {
|
||||
@@ -325,7 +327,7 @@ fn system_or_developer_message(
|
||||
}
|
||||
|
||||
/// Build a Harmony developer message with optional instructions and function tools.
|
||||
fn developer_message(instructions: Option<String>, tools: Option<&[ChatTool]>) -> Message {
|
||||
fn developer_message(instructions: Option<String>, tools: Option<&[Tool]>) -> Message {
|
||||
let mut content = DeveloperContent::new();
|
||||
if let Some(instructions) = instructions.filter(|text| !text.is_empty()) {
|
||||
content = content.with_instructions(instructions);
|
||||
@@ -427,11 +429,11 @@ fn auto_drop_analysis_messages(messages: Vec<Message>) -> Vec<Message> {
|
||||
|
||||
/// Flatten vLLM text content and reject unsupported multimodal parts.
|
||||
fn flatten_text(content: &ChatContent) -> Result<String> {
|
||||
content.try_flatten_to_text()
|
||||
content.try_flatten_to_text().map_err(Error::UnsupportedMultimodalContent)
|
||||
}
|
||||
|
||||
/// Convert vLLM function tool definitions to Harmony tool descriptions.
|
||||
fn to_tool_descriptions(tools: &[ChatTool]) -> Vec<ToolDescription> {
|
||||
fn to_tool_descriptions(tools: &[Tool]) -> Vec<ToolDescription> {
|
||||
tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
+23
-23
@@ -10,15 +10,15 @@ use super::HarmonyChatRenderer;
|
||||
use super::encoding::harmony_encoding;
|
||||
use crate::ChatRenderer;
|
||||
use crate::error::Error;
|
||||
use crate::event::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
use crate::request::{
|
||||
ChatContentPart, ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort,
|
||||
use crate::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
use crate::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::{
|
||||
ChatContentPart, ChatMessage, GenerationPromptMode, ReasoningEffort, TestRenderRequest,
|
||||
};
|
||||
|
||||
const PINNED_DATE: &str = "2025-06-28";
|
||||
|
||||
fn fixture_request(input_name: &str) -> ChatRequest {
|
||||
fn fixture_request(input_name: &str) -> TestRenderRequest {
|
||||
fixture_chat_request(
|
||||
&fixture_path(input_name),
|
||||
FixtureRequestOptions {
|
||||
@@ -30,7 +30,7 @@ fn fixture_request(input_name: &str) -> ChatRequest {
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/renderer/harmony")
|
||||
.join("src/harmony")
|
||||
.join("fixtures")
|
||||
.join(name)
|
||||
}
|
||||
@@ -39,24 +39,24 @@ fn test_renderer(use_system_instructions: bool) -> HarmonyChatRenderer {
|
||||
HarmonyChatRenderer::with_options(PINNED_DATE, use_system_instructions).unwrap()
|
||||
}
|
||||
|
||||
fn render_token_ids(request: &ChatRequest) -> Vec<u32> {
|
||||
fn render_token_ids(request: &TestRenderRequest) -> Vec<u32> {
|
||||
render_token_ids_with(&test_renderer(false), request)
|
||||
}
|
||||
|
||||
fn render_token_ids_with(renderer: &HarmonyChatRenderer, request: &ChatRequest) -> Vec<u32> {
|
||||
fn render_token_ids_with(renderer: &HarmonyChatRenderer, request: &TestRenderRequest) -> Vec<u32> {
|
||||
renderer
|
||||
.render(request)
|
||||
.render(request.as_request())
|
||||
.unwrap()
|
||||
.prompt
|
||||
.content
|
||||
.into_token_ids()
|
||||
.expect("Harmony renderer returns token IDs")
|
||||
}
|
||||
|
||||
fn render_prompt_text(request: &ChatRequest) -> String {
|
||||
fn render_prompt_text(request: &TestRenderRequest) -> String {
|
||||
render_prompt_text_with(&test_renderer(false), request)
|
||||
}
|
||||
|
||||
fn render_prompt_text_with(renderer: &HarmonyChatRenderer, request: &ChatRequest) -> String {
|
||||
fn render_prompt_text_with(renderer: &HarmonyChatRenderer, request: &TestRenderRequest) -> String {
|
||||
let token_ids = render_token_ids_with(renderer, request);
|
||||
harmony_encoding().unwrap().tokenizer().decode_utf8(&token_ids).unwrap()
|
||||
}
|
||||
@@ -137,10 +137,10 @@ fn drops_stale_analysis_fixture() {
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_reasoning_effort() {
|
||||
let mut request = ChatRequest::for_test();
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request.chat_options.reasoning_effort = Some(ReasoningEffort::None);
|
||||
|
||||
let error = test_renderer(false).render(&request).unwrap_err();
|
||||
let error = test_renderer(false).render(request.as_request()).unwrap_err();
|
||||
|
||||
expect![[r#"chat template error: reasoning_effort="none" is not supported by Harmony. Supported values are: low, medium, high."#]]
|
||||
.assert_eq(&error.to_report_string());
|
||||
@@ -148,7 +148,7 @@ fn rejects_invalid_reasoning_effort() {
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_tool_response_id() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::assistant_blocks(vec![AssistantContentBlock::ToolCall(
|
||||
AssistantToolCall {
|
||||
@@ -159,10 +159,10 @@ fn rejects_unknown_tool_response_id() {
|
||||
)]),
|
||||
ChatMessage::tool_response("{}", "call-unknown"),
|
||||
],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let error = test_renderer(false).render(&request).unwrap_err();
|
||||
let error = test_renderer(false).render(request.as_request()).unwrap_err();
|
||||
|
||||
expect![
|
||||
"chat template error: invalid Harmony tool message: unknown tool_call_id `call-unknown`"
|
||||
@@ -172,14 +172,14 @@ fn rejects_unknown_tool_response_id() {
|
||||
|
||||
#[test]
|
||||
fn rejects_multimodal_input() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::user(vec![ChatContentPart::image_url(
|
||||
"data:image/png;base64,test",
|
||||
)])],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let error = test_renderer(false).render(&request).unwrap_err();
|
||||
let error = test_renderer(false).render(request.as_request()).unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
@@ -189,16 +189,16 @@ fn rejects_multimodal_input() {
|
||||
|
||||
#[test]
|
||||
fn rejects_continue_final_assistant() {
|
||||
let mut request = ChatRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::user("write"),
|
||||
ChatMessage::assistant_text("partial"),
|
||||
],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
request.chat_options.generation_prompt_mode = GenerationPromptMode::ContinueFinalAssistant;
|
||||
|
||||
let error = test_renderer(false).render(&request).unwrap_err();
|
||||
let error = test_renderer(false).render(request.as_request()).unwrap_err();
|
||||
|
||||
expect!["chat template error: Harmony renderer does not support continue_final_message"]
|
||||
.assert_eq(&error.to_report_string());
|
||||
@@ -33,8 +33,11 @@ pub enum ChatTemplateContentFormatOption {
|
||||
}
|
||||
|
||||
impl ChatTemplateContentFormatOption {
|
||||
/// CLI/config literal for automatic content-format detection.
|
||||
pub const AUTO_LITERAL: &str = "auto";
|
||||
/// CLI/config literal for OpenAI-compatible structured content.
|
||||
pub const OPENAI_LITERAL: &str = "openai";
|
||||
/// CLI/config literal for flattened string content.
|
||||
pub const STRING_LITERAL: &str = "string";
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
//! Hugging Face Jinja chat-template renderer.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use thiserror_ext::AsReport as _;
|
||||
use tracing::{info, trace, warn};
|
||||
use vllm_text::Prompt;
|
||||
use vllm_text::backend::hf::{
|
||||
HfSpecialTokens, HfTokenizerConfig, ResolvedModelFiles, load_tokenizer_config,
|
||||
};
|
||||
use vllm_tokenizer::{HfSpecialTokens, HfTokenizerConfig, load_tokenizer_config};
|
||||
|
||||
use self::format::{
|
||||
ChatTemplateContentFormat, ChatTemplateContentFormatOption as ContentFormatOption,
|
||||
};
|
||||
use self::template::{CompiledChatTemplate, TemplateContext};
|
||||
use self::template::{
|
||||
CompiledChatTemplate, TemplateContext, load_chat_template, resolve_chat_template,
|
||||
};
|
||||
use self::value::{TemplateValue, to_template_value};
|
||||
use super::{ChatRenderer, RenderedPrompt, effective_template_kwargs};
|
||||
use super::{
|
||||
ChatRenderer, RenderRequest, RenderedPrompt, RenderedPromptContent, effective_template_kwargs,
|
||||
};
|
||||
use crate::error::Result;
|
||||
use crate::request::{ChatContent, ChatContentPart, ChatMessage, ChatRequest};
|
||||
use crate::{
|
||||
AssistantContentBlock, AssistantMessageExt, ChatTool, Error, LoadModelBackendsOptions,
|
||||
AssistantContentBlock, AssistantMessageExt, ChatContent, ChatContentPart, ChatMessage, Error,
|
||||
Tool,
|
||||
};
|
||||
|
||||
mod error;
|
||||
@@ -30,8 +34,6 @@ mod template;
|
||||
mod tojson;
|
||||
mod value;
|
||||
|
||||
pub use template::{load_chat_template, resolve_chat_template};
|
||||
|
||||
pub use self::format::ChatTemplateContentFormatOption;
|
||||
|
||||
/// Template-visible placeholder tokens per supported modality.
|
||||
@@ -40,11 +42,36 @@ pub use self::format::ChatTemplateContentFormatOption;
|
||||
/// content parts of that modality are rejected during rendering.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MultimodalRenderInfo {
|
||||
/// Template-visible image placeholder token.
|
||||
pub image_token: Option<String>,
|
||||
/// Template-visible video placeholder token.
|
||||
pub video_token: Option<String>,
|
||||
/// Template-visible audio placeholder token.
|
||||
pub audio_token: Option<String>,
|
||||
}
|
||||
|
||||
/// Resolved local files consumed while constructing an HF renderer.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct HfRendererFiles<'a> {
|
||||
/// Resolved `tokenizer_config.json`, when available.
|
||||
pub tokenizer_config: Option<&'a Path>,
|
||||
/// Resolved dedicated chat-template file, when available.
|
||||
pub chat_template: Option<&'a Path>,
|
||||
}
|
||||
|
||||
/// Model-level options used to construct an HF renderer.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HfRendererConfig {
|
||||
/// Optional inline or file-backed chat-template override.
|
||||
pub chat_template: Option<String>,
|
||||
/// Default template kwargs merged before request-level kwargs.
|
||||
pub default_template_kwargs: HashMap<String, JsonValue>,
|
||||
/// How `message.content` should be represented in the template.
|
||||
pub content_format: ContentFormatOption,
|
||||
/// Template-visible multimodal placeholder tokens.
|
||||
pub multimodal: Option<MultimodalRenderInfo>,
|
||||
}
|
||||
|
||||
/// Hugging Face chat-template renderer backed by the local Jinja chat-template
|
||||
/// state.
|
||||
pub struct HfChatRenderer {
|
||||
@@ -76,27 +103,25 @@ impl HfChatRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Attach named special tokens exposed to chat templates.
|
||||
pub fn with_special_tokens(mut self, special_tokens: Option<HfSpecialTokens>) -> Self {
|
||||
self.special_tokens = special_tokens;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach template-visible multimodal placeholder tokens.
|
||||
pub fn with_multimodal(mut self, multimodal: Option<MultimodalRenderInfo>) -> Self {
|
||||
self.multimodal = multimodal;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a renderer from the given model files and loading options.
|
||||
pub fn load(
|
||||
files: &ResolvedModelFiles,
|
||||
options: LoadModelBackendsOptions,
|
||||
multimodal: Option<MultimodalRenderInfo>,
|
||||
) -> Result<Self> {
|
||||
/// Create a renderer from resolved local model files and renderer options.
|
||||
pub fn load(files: HfRendererFiles<'_>, options: HfRendererConfig) -> Result<Self> {
|
||||
let HfTokenizerConfig {
|
||||
special_tokens,
|
||||
chat_template,
|
||||
..
|
||||
} = load_tokenizer_config(files.tokenizer_config_path.as_deref())?;
|
||||
} = load_tokenizer_config(files.tokenizer_config)?;
|
||||
let mut template = chat_template;
|
||||
let special_tokens = (!special_tokens.is_empty()).then_some(special_tokens);
|
||||
|
||||
@@ -106,7 +131,7 @@ impl HfChatRenderer {
|
||||
.map_err(|error| Error::ChatTemplate(error.to_report_string()))?,
|
||||
);
|
||||
info!("using configured chat template override");
|
||||
} else if let Some(chat_template_path) = files.chat_template_path.as_deref() {
|
||||
} else if let Some(chat_template_path) = files.chat_template {
|
||||
// If independent chat template file(s) exist and contain non-empty content,
|
||||
// they take priority over template entries in the tokenizer config
|
||||
let file_template = load_chat_template(chat_template_path)
|
||||
@@ -128,11 +153,11 @@ impl HfChatRenderer {
|
||||
|
||||
Ok(Self::new(
|
||||
template,
|
||||
options.default_chat_template_kwargs,
|
||||
options.chat_template_content_format,
|
||||
options.default_template_kwargs,
|
||||
options.content_format,
|
||||
)?
|
||||
.with_special_tokens(special_tokens)
|
||||
.with_multimodal(multimodal))
|
||||
.with_multimodal(options.multimodal))
|
||||
}
|
||||
|
||||
/// Apply the chat template to one chat request, rendering the prompt string
|
||||
@@ -141,7 +166,7 @@ impl HfChatRenderer {
|
||||
/// If the request carries a per-request `chat_template` override, a
|
||||
/// temporary template is compiled from that string and used instead of
|
||||
/// the model's default.
|
||||
fn apply_chat_template(&self, request: &ChatRequest) -> Result<RenderedPrompt> {
|
||||
fn apply_chat_template(&self, request: &RenderRequest<'_>) -> Result<RenderedPrompt> {
|
||||
let override_template = request
|
||||
.chat_options
|
||||
.chat_template
|
||||
@@ -162,10 +187,10 @@ impl HfChatRenderer {
|
||||
fn apply_chat_template_inner(
|
||||
&self,
|
||||
effective_template: &CompiledChatTemplate,
|
||||
request: &ChatRequest,
|
||||
request: &RenderRequest<'_>,
|
||||
) -> Result<RenderedPrompt> {
|
||||
let mut messages = to_template_messages(
|
||||
&request.messages,
|
||||
request.messages,
|
||||
effective_template.content_format(),
|
||||
self.multimodal.as_ref(),
|
||||
)?;
|
||||
@@ -181,7 +206,7 @@ impl HfChatRenderer {
|
||||
None
|
||||
};
|
||||
|
||||
let tools = request.tool_parsing_enabled().then(|| to_template_tools(&request.tools));
|
||||
let tools = request.tool_parsing_enabled().then(|| to_template_tools(request.tools));
|
||||
trace!(
|
||||
message_count = messages.len(),
|
||||
content_format = ?effective_template.content_format(),
|
||||
@@ -198,7 +223,7 @@ impl HfChatRenderer {
|
||||
add_generation_prompt: request.chat_options.add_generation_prompt(),
|
||||
continue_final_message: request.chat_options.continue_final_message(),
|
||||
tools: tools.as_deref(),
|
||||
documents: request.documents.as_deref(),
|
||||
documents: request.documents,
|
||||
template_kwargs: Some(&effective_template_kwargs),
|
||||
special_tokens: self.special_tokens.as_ref(),
|
||||
})
|
||||
@@ -217,15 +242,15 @@ impl HfChatRenderer {
|
||||
);
|
||||
|
||||
Ok(RenderedPrompt {
|
||||
prompt: Prompt::Text(prompt),
|
||||
content: RenderedPromptContent::Text(prompt),
|
||||
effective_template_kwargs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatRenderer for HfChatRenderer {
|
||||
fn render(&self, request: &ChatRequest) -> Result<RenderedPrompt> {
|
||||
self.apply_chat_template(request)
|
||||
fn render(&self, request: RenderRequest<'_>) -> Result<RenderedPrompt> {
|
||||
self.apply_chat_template(&request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,7 +581,7 @@ fn truncate_prompt_at_continue_final_message_tag(
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
fn to_template_tools(tools: &[ChatTool]) -> Vec<TemplateTool> {
|
||||
fn to_template_tools(tools: &[Tool]) -> Vec<TemplateTool> {
|
||||
tools
|
||||
.iter()
|
||||
.map(|tool| TemplateTool {
|
||||
@@ -577,42 +602,39 @@ mod tests {
|
||||
|
||||
use expect_test::expect;
|
||||
use serde_json::Value;
|
||||
use vllm_text::Prompt;
|
||||
use vllm_text::backend::hf::{HfSpecialTokens, NamedSpecialToken};
|
||||
use vllm_tokenizer::{HfSpecialTokens, NamedSpecialToken};
|
||||
|
||||
use super::{ChatTemplateContentFormatOption, HfChatRenderer, MultimodalRenderInfo};
|
||||
use crate::request::{
|
||||
ChatContentPart, ChatMessage, ChatRequest, ChatRole, ChatTool, ChatToolChoice,
|
||||
GenerationPromptMode, ReasoningEffort,
|
||||
use crate::{
|
||||
AssistantContentBlock, ChatContentPart, ChatMessage, ChatRenderer, ChatRole,
|
||||
ChatToolChoice, Error, GenerationPromptMode, ReasoningEffort, RenderedPromptContent,
|
||||
Result, TestRenderRequest, Tool,
|
||||
};
|
||||
use crate::{AssistantContentBlock, ChatRenderer, Error, Result};
|
||||
|
||||
const QWEN3_0_6B_TEMPLATE: &str = include_str!("../../../tests/templates/qwen3.jinja");
|
||||
const QWEN3_5_0_8B_TEMPLATE: &str = include_str!("../../../tests/templates/qwen35.jinja");
|
||||
const QWEN3_0_6B_TEMPLATE: &str = include_str!("../../../chat/tests/templates/qwen3.jinja");
|
||||
const QWEN3_5_0_8B_TEMPLATE: &str = include_str!("../../../chat/tests/templates/qwen35.jinja");
|
||||
|
||||
fn sample_request(messages: Vec<ChatMessage>) -> ChatRequest {
|
||||
ChatRequest {
|
||||
fn sample_request(messages: Vec<ChatMessage>) -> TestRenderRequest {
|
||||
TestRenderRequest {
|
||||
messages,
|
||||
request_id: "render-test".to_string(),
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
}
|
||||
}
|
||||
|
||||
fn render(template: Option<&str>, request: &ChatRequest) -> Result<String> {
|
||||
HfChatRenderer::new(
|
||||
fn render(template: Option<&str>, request: &TestRenderRequest) -> Result<String> {
|
||||
let rendered = HfChatRenderer::new(
|
||||
template.map(str::to_owned),
|
||||
HashMap::new(),
|
||||
ChatTemplateContentFormatOption::Auto,
|
||||
)?
|
||||
.render(request)?
|
||||
.prompt
|
||||
.into_text()
|
||||
.map_err(|_| unreachable!("HF renderer should return text prompt"))
|
||||
.render(request.as_request())?
|
||||
.content;
|
||||
Ok(rendered.into_text().expect("HF renderer should return text prompt"))
|
||||
}
|
||||
|
||||
fn render_mm(
|
||||
template: &str,
|
||||
request: &ChatRequest,
|
||||
request: &TestRenderRequest,
|
||||
content_format: ChatTemplateContentFormatOption,
|
||||
) -> Result<crate::RenderedPrompt> {
|
||||
HfChatRenderer::new(Some(template.to_string()), HashMap::new(), content_format)?
|
||||
@@ -621,10 +643,10 @@ mod tests {
|
||||
video_token: Some("<video>".to_string()),
|
||||
audio_token: Some("<audio>".to_string()),
|
||||
}))
|
||||
.render(request)
|
||||
.render(request.as_request())
|
||||
}
|
||||
|
||||
fn image_request() -> ChatRequest {
|
||||
fn image_request() -> TestRenderRequest {
|
||||
sample_request(vec![ChatMessage::user(vec![
|
||||
ChatContentPart::text("a"),
|
||||
ChatContentPart::image_url("data:image/png;base64,test"),
|
||||
@@ -632,7 +654,7 @@ mod tests {
|
||||
])])
|
||||
}
|
||||
|
||||
fn video_request() -> ChatRequest {
|
||||
fn video_request() -> TestRenderRequest {
|
||||
sample_request(vec![ChatMessage::user(vec![
|
||||
ChatContentPart::text("a"),
|
||||
ChatContentPart::video_url("https://example.com/demo.mp4"),
|
||||
@@ -640,7 +662,7 @@ mod tests {
|
||||
])])
|
||||
}
|
||||
|
||||
fn audio_request() -> ChatRequest {
|
||||
fn audio_request() -> TestRenderRequest {
|
||||
sample_request(vec![ChatMessage::user(vec![
|
||||
ChatContentPart::text("a"),
|
||||
ChatContentPart::input_audio("dGVzdA==", Some("wav".to_string())),
|
||||
@@ -658,7 +680,10 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendered.prompt, Prompt::Text("a<image>b".to_string()));
|
||||
assert_eq!(
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("a<image>b".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -670,7 +695,10 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendered.prompt, Prompt::Text("a<video>b".to_string()));
|
||||
assert_eq!(
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("a<video>b".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -683,8 +711,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
rendered.prompt,
|
||||
Prompt::Text("a<audio><audio>b".to_string())
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("a<audio><audio>b".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -697,7 +725,10 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendered.prompt, Prompt::Text("a<|image_pad|>b".to_string()));
|
||||
assert_eq!(
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("a<|image_pad|>b".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -709,7 +740,10 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendered.prompt, Prompt::Text("a<|video_pad|>b".to_string()));
|
||||
assert_eq!(
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("a<|video_pad|>b".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -722,8 +756,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
rendered.prompt,
|
||||
Prompt::Text("a<|audio_pad|><|audio_pad|>b".to_string())
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("a<|audio_pad|><|audio_pad|>b".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -740,7 +774,7 @@ mod tests {
|
||||
video_token: None,
|
||||
audio_token: None,
|
||||
}))
|
||||
.render(&video_request())
|
||||
.render(video_request().as_request())
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
@@ -842,12 +876,12 @@ mod tests {
|
||||
ChatTemplateContentFormatOption::OpenAi,
|
||||
)
|
||||
.unwrap()
|
||||
.prompt;
|
||||
.content;
|
||||
|
||||
// Anything rendered after the continued text (here the image
|
||||
// placeholder and the end marker) is truncated away, matching
|
||||
// transformers.
|
||||
assert_eq!(rendered, Prompt::Text("Sure,".to_string()));
|
||||
assert_eq!(rendered, RenderedPromptContent::Text("Sure,".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -904,7 +938,7 @@ mod tests {
|
||||
fn chat_template_exposes_developer_tools() {
|
||||
let request = sample_request(vec![ChatMessage::developer(
|
||||
"policy",
|
||||
Some(vec![ChatTool {
|
||||
Some(vec![Tool {
|
||||
name: "get_weather".to_string(),
|
||||
description: Some("Get weather".to_string()),
|
||||
parameters: serde_json::json!({
|
||||
@@ -1005,10 +1039,13 @@ mod tests {
|
||||
)
|
||||
.unwrap()
|
||||
.with_special_tokens(Some(special_tokens))
|
||||
.apply_chat_template(&request)
|
||||
.apply_chat_template(&request.as_request())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendered.prompt, Prompt::Text("<bos>|true".to_string()));
|
||||
assert_eq!(
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("<bos>|true".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1046,11 +1083,14 @@ mod tests {
|
||||
ChatTemplateContentFormatOption::String,
|
||||
)
|
||||
.unwrap()
|
||||
.render(&request)
|
||||
.render(request.as_request())
|
||||
.unwrap()
|
||||
.prompt;
|
||||
.content;
|
||||
|
||||
assert_eq!(rendered, Prompt::Text("hello world".to_string()));
|
||||
assert_eq!(
|
||||
rendered,
|
||||
RenderedPromptContent::Text("hello world".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1066,11 +1106,14 @@ mod tests {
|
||||
ChatTemplateContentFormatOption::OpenAi,
|
||||
)
|
||||
.unwrap()
|
||||
.render(&request)
|
||||
.render(request.as_request())
|
||||
.unwrap()
|
||||
.prompt;
|
||||
.content;
|
||||
|
||||
assert_eq!(rendered, Prompt::Text("hello world".to_string()));
|
||||
assert_eq!(
|
||||
rendered,
|
||||
RenderedPromptContent::Text("hello world".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1091,9 +1134,9 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let rendered = renderer.render(&request).unwrap().prompt;
|
||||
let rendered = renderer.render(request.as_request()).unwrap().content;
|
||||
|
||||
assert_eq!(rendered, Prompt::Text("true|x".to_string()));
|
||||
assert_eq!(rendered, RenderedPromptContent::Text("true|x".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1115,9 +1158,12 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let rendered = renderer.render(&request).unwrap();
|
||||
let rendered = renderer.render(request.as_request()).unwrap();
|
||||
|
||||
assert_eq!(rendered.prompt, Prompt::Text("max".to_string()));
|
||||
assert_eq!(
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("max".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
rendered.effective_template_kwargs.get("reasoning_effort"),
|
||||
Some(&Value::String("max".to_string()))
|
||||
@@ -1144,9 +1190,12 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let rendered = renderer.render(&request).unwrap();
|
||||
let rendered = renderer.render(request.as_request()).unwrap();
|
||||
|
||||
assert_eq!(rendered.prompt, Prompt::Text("none|true".to_string()));
|
||||
assert_eq!(
|
||||
rendered.content,
|
||||
RenderedPromptContent::Text("none|true".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
rendered.effective_template_kwargs.get("reasoning_effort"),
|
||||
Some(&Value::String("none".to_string()))
|
||||
@@ -1222,7 +1271,7 @@ mod tests {
|
||||
#[test]
|
||||
fn chat_template_exposes_tools_to_templates_when_auto_enabled() {
|
||||
let mut request = sample_request(vec![ChatMessage::text(ChatRole::User, "hello")]);
|
||||
request.tools = vec![ChatTool {
|
||||
request.tools = vec![Tool {
|
||||
name: "get_weather".to_string(),
|
||||
description: Some("Get weather".to_string()),
|
||||
parameters: serde_json::json!({
|
||||
@@ -1326,7 +1375,7 @@ mod tests {
|
||||
"<|im_start|>user\na<|vision_start|><|image_pad|><|vision_end|>b<|im_end|>\n",
|
||||
)
|
||||
"#]]
|
||||
.assert_debug_eq(&rendered.prompt);
|
||||
.assert_debug_eq(&rendered.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
+6
-6
@@ -5,7 +5,7 @@
|
||||
//!
|
||||
//! This module is inlined from SMG's tokenizer crate with local adaptations:
|
||||
//! - thinking-related detection/state is removed
|
||||
//! - special tokens are wired to `vllm_text::backends::hf::HfSpecialTokens`
|
||||
//! - special tokens are wired to `vllm_tokenizer::HfSpecialTokens`
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
@@ -14,14 +14,14 @@ use std::path::Path;
|
||||
use minijinja::Environment;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{self};
|
||||
use vllm_text::backend::hf::HfSpecialTokens;
|
||||
use vllm_tokenizer::HfSpecialTokens;
|
||||
|
||||
use super::error::TemplateError;
|
||||
use super::format::{
|
||||
ChatTemplateContentFormat, ChatTemplateContentFormatOption, detect_chat_template_content_format,
|
||||
};
|
||||
use super::tojson::hf_tojson_filter;
|
||||
use crate::renderer::hf::{TemplateMessage, TemplateTool};
|
||||
use crate::hf::{TemplateMessage, TemplateTool};
|
||||
|
||||
type Result<T> = std::result::Result<T, TemplateError>;
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(super) struct TemplateContext<'a> {
|
||||
}
|
||||
|
||||
/// Load chat template from a file (`.jinja` or `.json` containing Jinja).
|
||||
pub fn load_chat_template(template_path: &Path) -> Result<Option<String>> {
|
||||
pub(crate) fn load_chat_template(template_path: &Path) -> Result<Option<String>> {
|
||||
let content = fs::read_to_string(template_path).map_err(TemplateError::ReadTemplateFile)?;
|
||||
|
||||
if template_path.extension().is_some_and(|ext| ext == "json") {
|
||||
@@ -82,7 +82,7 @@ pub fn load_chat_template(template_path: &Path) -> Result<Option<String>> {
|
||||
}
|
||||
|
||||
/// Resolve a configured chat template value into a template string.
|
||||
pub fn resolve_chat_template(chat_template: &str) -> Result<String> {
|
||||
pub(crate) fn resolve_chat_template(chat_template: &str) -> Result<String> {
|
||||
let path = Path::new(chat_template);
|
||||
if path.exists() {
|
||||
return load_chat_template(path).map(|template| template.unwrap_or_default());
|
||||
@@ -136,7 +136,7 @@ mod tests {
|
||||
use std::fs;
|
||||
|
||||
use tempfile::TempDir;
|
||||
use vllm_text::backend::hf::{HfSpecialTokens, NamedSpecialToken};
|
||||
use vllm_tokenizer::{HfSpecialTokens, NamedSpecialToken};
|
||||
|
||||
use super::*;
|
||||
|
||||
+16
-14
@@ -5,13 +5,15 @@ use std::collections::HashMap;
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use thiserror_ext::AsReport as _;
|
||||
use vllm_text::Prompt;
|
||||
use vllm_text::tokenizer::{DynTokenizer, Tokenizer};
|
||||
use vllm_tokenizer::{DynTokenizer, Tokenizer};
|
||||
|
||||
use super::{ChatRenderer, RenderedPrompt, request_template_kwargs};
|
||||
use super::{
|
||||
ChatRenderer, RenderRequest, RenderedPrompt, RenderedPromptContent, request_template_kwargs,
|
||||
};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::request::{ChatContent, ChatContentPart, ChatMessage, ChatRequest, ChatTool};
|
||||
use crate::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::{
|
||||
AssistantContentBlock, AssistantToolCall, ChatContent, ChatContentPart, ChatMessage, Tool,
|
||||
};
|
||||
|
||||
const MESSAGE_USER: &str = "<|message_user|>";
|
||||
const MESSAGE_MODEL: &str = "<|message_model|>";
|
||||
@@ -170,7 +172,7 @@ impl InklingChatRenderer {
|
||||
)
|
||||
}
|
||||
|
||||
fn write_tool_declarations(&self, out: &mut Vec<u32>, tools: &[&ChatTool]) -> Result<()> {
|
||||
fn write_tool_declarations(&self, out: &mut Vec<u32>, tools: &[&Tool]) -> Result<()> {
|
||||
if tools.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -277,14 +279,14 @@ impl InklingChatRenderer {
|
||||
tool_call_id: &str,
|
||||
tool_call_id_to_name: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
let text = content.try_flatten_to_text()?;
|
||||
let text = content.try_flatten_to_text().map_err(Error::UnsupportedMultimodalContent)?;
|
||||
let tool_name = tool_call_id_to_name.get(tool_call_id).map(String::as_str).unwrap_or("");
|
||||
self.write_text_block(out, self.special.message_tool, Some(tool_name), &text)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatRenderer for InklingChatRenderer {
|
||||
fn render(&self, request: &ChatRequest) -> Result<RenderedPrompt> {
|
||||
fn render(&self, request: RenderRequest<'_>) -> Result<RenderedPrompt> {
|
||||
request.validate()?;
|
||||
if request.chat_options.continue_final_message() {
|
||||
return Err(Error::ChatTemplate(
|
||||
@@ -294,13 +296,13 @@ impl ChatRenderer for InklingChatRenderer {
|
||||
|
||||
let mut out = Vec::new();
|
||||
let mut tool_call_id_to_name = HashMap::new();
|
||||
let effective_template_kwargs = request_template_kwargs(request);
|
||||
let tools = rendered_tools(request);
|
||||
let effective_template_kwargs = request_template_kwargs(&request);
|
||||
let tools = rendered_tools(&request);
|
||||
self.write_tool_declarations(&mut out, &tools)?;
|
||||
let mut reasoning_effort =
|
||||
resolve_reasoning_effort(effective_template_kwargs.get("reasoning_effort"));
|
||||
|
||||
for message in &request.messages {
|
||||
for message in request.messages {
|
||||
if !matches!(
|
||||
message,
|
||||
ChatMessage::System { .. } | ChatMessage::Developer { .. }
|
||||
@@ -345,7 +347,7 @@ impl ChatRenderer for InklingChatRenderer {
|
||||
}
|
||||
|
||||
Ok(RenderedPrompt {
|
||||
prompt: Prompt::TokenIds(out),
|
||||
content: RenderedPromptContent::TokenIds(out),
|
||||
effective_template_kwargs,
|
||||
})
|
||||
}
|
||||
@@ -378,14 +380,14 @@ fn resolve_special_token(tokenizer: &dyn Tokenizer, token: &str) -> Result<u32>
|
||||
})
|
||||
}
|
||||
|
||||
fn rendered_tools(request: &ChatRequest) -> Vec<&ChatTool> {
|
||||
fn rendered_tools<'a>(request: &RenderRequest<'a>) -> Vec<&'a Tool> {
|
||||
if !request.tool_parsing_enabled() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut tools = Vec::with_capacity(request.tools.len());
|
||||
tools.extend(request.tools.iter());
|
||||
for message in &request.messages {
|
||||
for message in request.messages {
|
||||
if let ChatMessage::Developer {
|
||||
tools: Some(local_tools),
|
||||
..
|
||||
+27
-31
@@ -7,27 +7,23 @@ use std::sync::Arc;
|
||||
use expect_test::{ExpectFile, expect_file};
|
||||
use serde_json::json;
|
||||
use thiserror_ext::AsReport;
|
||||
use vllm_text::tokenizer::Tokenizer;
|
||||
use vllm_tokenizer::Tokenizer;
|
||||
|
||||
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
use crate::test_utils::{FixtureRequestOptions, fixture_chat_request};
|
||||
|
||||
use super::{
|
||||
AUDIO_END, CONTENT_AUDIO_INPUT, CONTENT_IMAGE, CONTENT_INVOKE_TOOL_JSON, CONTENT_TEXT,
|
||||
CONTENT_THINKING, CONTENT_XML, END_MESSAGE, InklingChatRenderer, MESSAGE_MODEL, MESSAGE_SYSTEM,
|
||||
MESSAGE_TOOL, MESSAGE_USER,
|
||||
};
|
||||
use crate::event::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::request::{ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort};
|
||||
use crate::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::{ChatMessage, GenerationPromptMode, ReasoningEffort, TestRenderRequest};
|
||||
use crate::{ChatRenderer, Error};
|
||||
|
||||
struct FixtureTokenizer;
|
||||
|
||||
impl Tokenizer for FixtureTokenizer {
|
||||
fn encode(
|
||||
&self,
|
||||
text: &str,
|
||||
_add_special_tokens: bool,
|
||||
) -> vllm_text::tokenizer::Result<Vec<u32>> {
|
||||
fn encode(&self, text: &str, _add_special_tokens: bool) -> vllm_tokenizer::Result<Vec<u32>> {
|
||||
Ok(text.bytes().map(u32::from).collect())
|
||||
}
|
||||
|
||||
@@ -35,7 +31,7 @@ impl Tokenizer for FixtureTokenizer {
|
||||
&self,
|
||||
token_ids: &[u32],
|
||||
_skip_special_tokens: bool,
|
||||
) -> vllm_text::tokenizer::Result<String> {
|
||||
) -> vllm_tokenizer::Result<String> {
|
||||
Ok(token_ids
|
||||
.iter()
|
||||
.map(|token_id| char::from_u32(*token_id).unwrap_or('\u{FFFD}'))
|
||||
@@ -86,16 +82,16 @@ fn renderer() -> InklingChatRenderer {
|
||||
InklingChatRenderer::new(Arc::new(FixtureTokenizer)).unwrap()
|
||||
}
|
||||
|
||||
fn render_token_ids(request: &ChatRequest) -> Vec<u32> {
|
||||
fn render_token_ids(request: &TestRenderRequest) -> Vec<u32> {
|
||||
renderer()
|
||||
.render(request)
|
||||
.render(request.as_request())
|
||||
.unwrap()
|
||||
.prompt
|
||||
.content
|
||||
.into_token_ids()
|
||||
.expect("Inkling renderer returns token IDs")
|
||||
}
|
||||
|
||||
fn fixture_request(name: &str) -> ChatRequest {
|
||||
fn fixture_request(name: &str) -> TestRenderRequest {
|
||||
fixture_chat_request(&fixture_path(name), inkling_fixture_options())
|
||||
}
|
||||
|
||||
@@ -108,7 +104,7 @@ fn inkling_fixture_options() -> FixtureRequestOptions {
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/renderer/inkling")
|
||||
.join("src/inkling")
|
||||
.join("fixtures")
|
||||
.join(name)
|
||||
}
|
||||
@@ -229,14 +225,14 @@ fn renders_named_reasoning_effort_after_tool_declarations() {
|
||||
|
||||
#[test]
|
||||
fn emits_one_reasoning_effort_for_multi_turn_conversation() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![
|
||||
ChatMessage::system("rules"),
|
||||
ChatMessage::user("user1"),
|
||||
ChatMessage::assistant_text("assistant1"),
|
||||
ChatMessage::user("user2"),
|
||||
],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let rendered = render_symbolic_tokens(&render_token_ids(&request));
|
||||
@@ -253,7 +249,7 @@ fn emits_one_reasoning_effort_for_multi_turn_conversation() {
|
||||
#[test]
|
||||
fn canonicalizes_numeric_zero_reasoning_effort() {
|
||||
for value in [0.0, -0.0] {
|
||||
let mut request = ChatRequest::for_test();
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request
|
||||
.chat_options
|
||||
.template_kwargs
|
||||
@@ -270,7 +266,7 @@ fn canonicalizes_numeric_zero_reasoning_effort() {
|
||||
|
||||
#[test]
|
||||
fn defaults_reasoning_effort_to_high() {
|
||||
let request = ChatRequest::for_test();
|
||||
let request = TestRenderRequest::for_test();
|
||||
|
||||
assert!(
|
||||
render_symbolic_tokens(&render_token_ids(&request)).starts_with(
|
||||
@@ -282,7 +278,7 @@ fn defaults_reasoning_effort_to_high() {
|
||||
|
||||
#[test]
|
||||
fn renders_numeric_reasoning_effort_template_kwarg() {
|
||||
let mut request = ChatRequest::for_test();
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request
|
||||
.chat_options
|
||||
.template_kwargs
|
||||
@@ -299,7 +295,7 @@ fn renders_numeric_reasoning_effort_template_kwarg() {
|
||||
#[test]
|
||||
fn ignores_unsupported_reasoning_effort_values() {
|
||||
for value in [json!(true), json!("invalid"), json!(null)] {
|
||||
let mut request = ChatRequest::for_test();
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request
|
||||
.chat_options
|
||||
.template_kwargs
|
||||
@@ -315,38 +311,38 @@ fn ignores_unsupported_reasoning_effort_values() {
|
||||
#[test]
|
||||
fn rejects_out_of_range_reasoning_effort() {
|
||||
for value in [0.990_000_1, 1.0, 1.5, -0.1] {
|
||||
let mut request = ChatRequest::for_test();
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request
|
||||
.chat_options
|
||||
.template_kwargs
|
||||
.insert("reasoning_effort".to_string(), json!(value));
|
||||
|
||||
let error = renderer().render(&request).unwrap_err();
|
||||
let error = renderer().render(request.as_request()).unwrap_err();
|
||||
assert!(error.as_report().to_string().contains("must be in [0.0, 0.99]"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_continue_final_message() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::assistant_text("partial")],
|
||||
chat_options: crate::ChatOptions {
|
||||
generation_prompt_mode: GenerationPromptMode::ContinueFinalAssistant,
|
||||
..Default::default()
|
||||
},
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let error = renderer().render(&request).unwrap_err();
|
||||
let error = renderer().render(request.as_request()).unwrap_err();
|
||||
assert!(matches!(error, Error::ChatTemplate(_)));
|
||||
assert!(error.as_report().to_string().contains("continue_final_message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_developer_messages_as_system() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::developer("rules", None)],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -359,7 +355,7 @@ fn renders_developer_messages_as_system() {
|
||||
|
||||
#[test]
|
||||
fn rejects_non_object_tool_call_arguments() {
|
||||
let request = ChatRequest {
|
||||
let request = TestRenderRequest {
|
||||
messages: vec![ChatMessage::assistant_blocks(vec![
|
||||
AssistantContentBlock::ToolCall(AssistantToolCall {
|
||||
id: "call_1".to_string(),
|
||||
@@ -367,9 +363,9 @@ fn rejects_non_object_tool_call_arguments() {
|
||||
arguments: "[]".to_string(),
|
||||
}),
|
||||
])],
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
let error = renderer().render(&request).unwrap_err();
|
||||
let error = renderer().render(request.as_request()).unwrap_err();
|
||||
assert!(error.as_report().to_string().contains("JSON object"));
|
||||
}
|
||||
@@ -1,45 +1,70 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
//! Engine-independent chat prompt renderers.
|
||||
//!
|
||||
//! The crate accepts borrowed chat-domain inputs and produces text or token-ID
|
||||
//! prompt artifacts. Serving requests, sampling policy, multimodal
|
||||
//! preprocessing, parser state, and engine transport remain in consumer
|
||||
//! crates.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use enum_as_inner::EnumAsInner;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use vllm_text::Prompt;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::request::{ChatRequest, ReasoningEffort};
|
||||
|
||||
pub mod deepseek_v32;
|
||||
pub mod deepseek_v4;
|
||||
mod deepseek_v32;
|
||||
mod deepseek_v4;
|
||||
mod error;
|
||||
pub mod harmony;
|
||||
pub mod hf;
|
||||
mod inkling;
|
||||
mod request;
|
||||
mod selection;
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
#[cfg(test)]
|
||||
pub(crate) use test_utils::TestRenderRequest;
|
||||
|
||||
pub use deepseek_v4::DeepSeekV4ChatRenderer;
|
||||
pub use deepseek_v32::DeepSeekV32ChatRenderer;
|
||||
pub use error::{Error, Result};
|
||||
pub use harmony::HarmonyChatRenderer;
|
||||
pub use inkling::InklingChatRenderer;
|
||||
pub use request::RenderRequest;
|
||||
pub use selection::RendererSelection;
|
||||
pub use vllm_chat_types::{
|
||||
AssistantBlockKind, AssistantContentBlock, AssistantMessage, AssistantMessageExt,
|
||||
AssistantToolCall, ChatContent, ChatContentPart, ChatMessage, ChatOptions, ChatRole,
|
||||
ChatToolChoice, GenerationPromptMode, ImageDetail, ReasoningEffort, Tool,
|
||||
};
|
||||
|
||||
/// Rendered chat prompt submitted to the text backend.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Engine-independent prompt content produced by a chat renderer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumAsInner)]
|
||||
#[serde(untagged)]
|
||||
pub enum RenderedPromptContent {
|
||||
/// Untokenized prompt text.
|
||||
Text(String),
|
||||
/// Prompt token IDs produced by a format-specific renderer.
|
||||
TokenIds(Vec<u32>),
|
||||
}
|
||||
|
||||
/// Rendered chat prompt plus effective template metadata.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RenderedPrompt {
|
||||
/// The rendered prompt, either as text or already tokenized.
|
||||
pub prompt: Prompt,
|
||||
/// Engine-independent prompt content.
|
||||
pub content: RenderedPromptContent,
|
||||
/// Effective chat-template kwargs visible to the renderer after applying
|
||||
/// server defaults, request overrides, and typed reasoning controls.
|
||||
pub effective_template_kwargs: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Minimal chat-prompt renderer used by `vllm-chat`.
|
||||
/// Synchronous chat-prompt renderer.
|
||||
pub trait ChatRenderer: Send + Sync {
|
||||
/// Render one chat request into the text prompt submitted to the text
|
||||
/// backend.
|
||||
fn render(&self, request: &ChatRequest) -> Result<RenderedPrompt>;
|
||||
/// Render one borrowed chat request into text or token IDs.
|
||||
fn render(&self, request: RenderRequest<'_>) -> Result<RenderedPrompt>;
|
||||
}
|
||||
|
||||
/// Shared trait-object form of [`ChatRenderer`].
|
||||
@@ -49,7 +74,7 @@ pub type DynChatRenderer = Arc<dyn ChatRenderer>;
|
||||
/// using the provided defaults as the base.
|
||||
pub(crate) fn effective_template_kwargs(
|
||||
default_template_kwargs: &HashMap<String, Value>,
|
||||
request: &ChatRequest,
|
||||
request: &RenderRequest<'_>,
|
||||
) -> HashMap<String, Value> {
|
||||
let mut kwargs = default_template_kwargs.clone();
|
||||
kwargs.extend(request.chat_options.template_kwargs.clone());
|
||||
@@ -71,6 +96,6 @@ pub(crate) fn effective_template_kwargs(
|
||||
}
|
||||
|
||||
/// Extract the effective chat-template kwargs visible to the renderer from the request.
|
||||
pub(crate) fn request_template_kwargs(request: &ChatRequest) -> HashMap<String, Value> {
|
||||
pub(crate) fn request_template_kwargs(request: &RenderRequest<'_>) -> HashMap<String, Value> {
|
||||
effective_template_kwargs(&HashMap::new(), request)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use serde_json::Value;
|
||||
use vllm_chat_types::{
|
||||
ChatMessage, ChatOptions, ChatRole, ChatToolChoice, GenerationPromptMode, Tool,
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Borrowed input consumed by one chat renderer.
|
||||
///
|
||||
/// This view contains only chat-domain values that affect prompt construction.
|
||||
/// Serving, sampling, decoding, scheduling, and engine metadata stay with the
|
||||
/// caller.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RenderRequest<'a> {
|
||||
/// Ordered chat history to render.
|
||||
pub messages: &'a [ChatMessage],
|
||||
/// Chat-template and generation-prompt controls.
|
||||
pub chat_options: &'a ChatOptions,
|
||||
/// Request-level tools available to the model.
|
||||
pub tools: &'a [Tool],
|
||||
/// Tool-choice behavior used to decide whether tools are exposed.
|
||||
pub tool_choice: &'a ChatToolChoice,
|
||||
/// Optional retrieval documents exposed to HF chat templates.
|
||||
pub documents: Option<&'a [Value]>,
|
||||
}
|
||||
|
||||
impl RenderRequest<'_> {
|
||||
/// Validate renderer-owned request invariants.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.messages.is_empty() {
|
||||
return Err(Error::EmptyMessages);
|
||||
}
|
||||
match (
|
||||
self.chat_options.generation_prompt_mode,
|
||||
self.messages.last().map(ChatMessage::role),
|
||||
) {
|
||||
(GenerationPromptMode::ContinueFinalAssistant, Some(ChatRole::Assistant)) => {}
|
||||
(GenerationPromptMode::ContinueFinalAssistant, _) => {
|
||||
return Err(Error::ContinueFinalAssistantWithoutFinalAssistant);
|
||||
}
|
||||
(GenerationPromptMode::NoGenerationPrompt, _)
|
||||
| (GenerationPromptMode::StartNewAssistant, _) => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return whether any message contains multimodal content.
|
||||
pub fn has_multimodal(&self) -> bool {
|
||||
self.messages.iter().any(ChatMessage::has_multimodal)
|
||||
}
|
||||
|
||||
/// Return whether request-level tools should be exposed to the renderer.
|
||||
pub fn tool_parsing_enabled(&self) -> bool {
|
||||
!matches!(self.tool_choice, ChatToolChoice::None) && !self.tools.is_empty()
|
||||
}
|
||||
|
||||
/// Return the request-level thinking toggle when explicitly requested.
|
||||
///
|
||||
/// The `thinking` and `enable_thinking` kwargs must be booleans when
|
||||
/// present and must carry the same value when both are set.
|
||||
pub fn enable_thinking(&self) -> Result<Option<bool>> {
|
||||
let thinking = self.parse_template_bool("thinking")?;
|
||||
let enable_thinking = self.parse_template_bool("enable_thinking")?;
|
||||
|
||||
match (thinking, enable_thinking) {
|
||||
(None, None) => Ok(None),
|
||||
(Some(thinking), Some(enable_thinking)) if thinking != enable_thinking => {
|
||||
Err(Error::ChatTemplate(
|
||||
"template kwargs `thinking` and `enable_thinking` must match when both are set"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
(Some(thinking), _) => Ok(Some(thinking)),
|
||||
(None, Some(enable_thinking)) => Ok(Some(enable_thinking)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse one optional boolean chat-template kwarg.
|
||||
pub fn parse_template_bool(&self, key: &str) -> Result<Option<bool>> {
|
||||
match self.chat_options.template_kwargs.get(key) {
|
||||
None => Ok(None),
|
||||
Some(Value::Bool(value)) => Ok(Some(*value)),
|
||||
Some(other) => Err(Error::ChatTemplate(format!(
|
||||
"template kwarg `{key}` must be a boolean, got {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::RenderRequest;
|
||||
use crate::{ChatMessage, ChatToolChoice, Error, GenerationPromptMode, TestRenderRequest};
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_message_history() {
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request.messages.clear();
|
||||
|
||||
assert!(matches!(
|
||||
request.as_request().validate(),
|
||||
Err(Error::EmptyMessages)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuation_requires_a_final_assistant_message() {
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request.chat_options.generation_prompt_mode = GenerationPromptMode::ContinueFinalAssistant;
|
||||
|
||||
assert!(matches!(
|
||||
request.as_request().validate(),
|
||||
Err(Error::ContinueFinalAssistantWithoutFinalAssistant)
|
||||
));
|
||||
|
||||
request.messages.push(ChatMessage::assistant_text("partial"));
|
||||
request.as_request().validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_aliases_must_match() {
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request
|
||||
.chat_options
|
||||
.template_kwargs
|
||||
.insert("thinking".to_string(), json!(false));
|
||||
request
|
||||
.chat_options
|
||||
.template_kwargs
|
||||
.insert("enable_thinking".to_string(), json!(true));
|
||||
|
||||
assert!(matches!(
|
||||
request.as_request().enable_thinking(),
|
||||
Err(Error::ChatTemplate(message))
|
||||
if message.contains("`thinking` and `enable_thinking` must match")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_exposure_requires_tools_and_an_enabled_choice() {
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
assert!(!request.as_request().tool_parsing_enabled());
|
||||
|
||||
request.tools.push(crate::Tool {
|
||||
name: "lookup".to_string(),
|
||||
description: None,
|
||||
parameters: json!({"type": "object"}),
|
||||
strict: None,
|
||||
});
|
||||
assert!(!request.as_request().tool_parsing_enabled());
|
||||
|
||||
request.tool_choice = ChatToolChoice::Auto;
|
||||
assert!(request.as_request().tool_parsing_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borrowed_request_carries_optional_documents() {
|
||||
let mut request = TestRenderRequest::for_test();
|
||||
request.documents = Some(vec![json!({"title": "doc"})]);
|
||||
|
||||
let borrowed: RenderRequest<'_> = request.as_request();
|
||||
assert_eq!(
|
||||
borrowed.documents.unwrap(),
|
||||
request.documents.as_deref().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,13 +29,21 @@ pub enum RendererSelection {
|
||||
}
|
||||
|
||||
impl RendererSelection {
|
||||
/// CLI/config literal for automatic renderer selection.
|
||||
pub const AUTO_LITERAL: &str = "auto";
|
||||
/// CLI/config literal and model type for DeepSeek V3.2.
|
||||
pub const DEEPSEEK_V32_LITERAL: &str = "deepseek_v32";
|
||||
/// CLI/config literal and model type for DeepSeek V4.
|
||||
pub const DEEPSEEK_V4_LITERAL: &str = "deepseek_v4";
|
||||
/// Hugging Face model type routed to the Harmony renderer.
|
||||
pub const GPT_OSS_MODEL_TYPE: &str = "gpt_oss";
|
||||
/// CLI/config literal for the Harmony renderer.
|
||||
pub const HARMONY_LITERAL: &str = "harmony";
|
||||
/// CLI/config literal for the Hugging Face renderer.
|
||||
pub const HF_LITERAL: &str = "hf";
|
||||
/// CLI/config literal for the Inkling renderer.
|
||||
pub const INKLING_LITERAL: &str = "inkling";
|
||||
/// Hugging Face model type routed to the Inkling renderer.
|
||||
pub const INKLING_MODEL_TYPE: &str = "inkling_mm_model";
|
||||
|
||||
/// Resolve the renderer selection using the given model type string, if
|
||||
+47
-14
@@ -7,13 +7,45 @@ use std::path::Path;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::event::{AssistantContentBlock, AssistantToolCall};
|
||||
use crate::request::{
|
||||
ChatContent, ChatContentPart, ChatMessage, ChatRequest, ChatTool, ChatToolChoice,
|
||||
GenerationPromptMode, ReasoningEffort,
|
||||
use crate::{
|
||||
AssistantContentBlock, AssistantToolCall, ChatContent, ChatContentPart, ChatMessage,
|
||||
ChatOptions, ChatRole, ChatToolChoice, GenerationPromptMode, ReasoningEffort, RenderRequest,
|
||||
Tool,
|
||||
};
|
||||
|
||||
/// Options for constructing a [`ChatRequest`] from a fixture file.
|
||||
/// Owned test input used to construct borrowed [`RenderRequest`] values.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TestRenderRequest {
|
||||
pub messages: Vec<ChatMessage>,
|
||||
pub chat_options: ChatOptions,
|
||||
pub tools: Vec<Tool>,
|
||||
pub tool_choice: ChatToolChoice,
|
||||
pub documents: Option<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl TestRenderRequest {
|
||||
pub(crate) fn for_test() -> Self {
|
||||
Self {
|
||||
messages: vec![ChatMessage::text(ChatRole::User, "test")],
|
||||
chat_options: ChatOptions::default(),
|
||||
tools: Vec::new(),
|
||||
tool_choice: ChatToolChoice::None,
|
||||
documents: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_request(&self) -> RenderRequest<'_> {
|
||||
RenderRequest {
|
||||
messages: &self.messages,
|
||||
chat_options: &self.chat_options,
|
||||
tools: &self.tools,
|
||||
tool_choice: &self.tool_choice,
|
||||
documents: self.documents.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for constructing a [`TestRenderRequest`] from a fixture file.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct FixtureRequestOptions {
|
||||
/// Whether to set the template kwarg `[enable_]thinking=true`.
|
||||
@@ -23,9 +55,11 @@ pub(crate) struct FixtureRequestOptions {
|
||||
pub no_generation_prompt_when_last_assistant: bool,
|
||||
}
|
||||
|
||||
/// Read a fixture file from the given path and convert it into a [`ChatRequest`]
|
||||
/// using the provided options.
|
||||
pub(crate) fn fixture_chat_request(path: &Path, options: FixtureRequestOptions) -> ChatRequest {
|
||||
/// Read a fixture file and convert it into an owned renderer test request.
|
||||
pub(crate) fn fixture_chat_request(
|
||||
path: &Path,
|
||||
options: FixtureRequestOptions,
|
||||
) -> TestRenderRequest {
|
||||
let fixture = fs::read_to_string(path).unwrap();
|
||||
let fixture: FixtureFile = serde_json::from_str(&fixture).unwrap();
|
||||
fixture.into_request().into_chat_request(options)
|
||||
@@ -135,9 +169,8 @@ struct FixtureToolCallFunction {
|
||||
}
|
||||
|
||||
impl FixtureRequest {
|
||||
fn into_chat_request(self, options: FixtureRequestOptions) -> ChatRequest {
|
||||
let mut request = ChatRequest {
|
||||
request_id: "renderer-fixture".to_string(),
|
||||
fn into_chat_request(self, options: FixtureRequestOptions) -> TestRenderRequest {
|
||||
let mut request = TestRenderRequest {
|
||||
messages: self
|
||||
.messages
|
||||
.into_iter()
|
||||
@@ -150,7 +183,7 @@ impl FixtureRequest {
|
||||
} else {
|
||||
ChatToolChoice::Auto
|
||||
},
|
||||
..ChatRequest::for_test()
|
||||
..TestRenderRequest::for_test()
|
||||
};
|
||||
|
||||
if options.no_generation_prompt_when_last_assistant
|
||||
@@ -243,10 +276,10 @@ fn to_chat_content(content: FixtureContent) -> ChatContent {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_chat_tools(tools: &[FixtureTool]) -> Vec<ChatTool> {
|
||||
fn to_chat_tools(tools: &[FixtureTool]) -> Vec<Tool> {
|
||||
tools
|
||||
.iter()
|
||||
.map(|tool| ChatTool {
|
||||
.map(|tool| Tool {
|
||||
name: tool.function.name.clone(),
|
||||
description: tool.function.description.clone(),
|
||||
parameters: tool.function.parameters.clone(),
|
||||
@@ -0,0 +1,6 @@
|
||||
# vLLM Example Templates
|
||||
|
||||
These fixtures are copied from `vllm/examples/`.
|
||||
|
||||
They are used by `src/chat-renderer/src/hf/format.rs` tests to keep
|
||||
chat-template content-format detection aligned with Python vLLM behavior.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user