Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 134 additions & 6 deletions tests/core/framework/chat_template/deepseek_v4_cpp_template_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ TEST(DeepseekV4CppTemplate, BasicChatModeUserMessage) {
messages.emplace_back("system", "You are a helpful assistant.");
messages.emplace_back("user", "Hello");

// Chat mode has to be asked for explicitly: an absent thinking flag now
// means thinking. See BareRequestDefaultsToThinkingWithHighEffort.
nlohmann::ordered_json kwargs = nlohmann::json::object();
kwargs["thinking"] = false;
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

Expand All @@ -74,6 +77,28 @@ TEST(DeepseekV4CppTemplate, BasicChatModeUserMessage) {
"<|User|>Hello<|Assistant|></think>");
}

// A request carrying no thinking flag at all defaults to thinking with the
// "high" effort tier, matching vLLM's DeepSeek-V4 tokenizer wrapper
// (thinking_enabled = True when neither "thinking" nor "enable_thinking" is
// present, then reasoning_effort = "high").
TEST(DeepseekV4CppTemplate, BareRequestDefaultsToThinkingWithHighEffort) {
auto encoder = make_encoder();

ChatMessages messages;
messages.emplace_back("user", "Hello");

nlohmann::ordered_json kwargs = nlohmann::json::object();
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

EXPECT_NE(prompt->find("Reasoning Effort: Absolute maximum"),
std::string::npos);
EXPECT_EQ(prompt->find("Reasoning Effort: Beyond maximum"),
std::string::npos);
EXPECT_NE(prompt->find("<|User|>Hello<|Assistant|><think>"),
std::string::npos);
}

TEST(DeepseekV4CppTemplate, ThinkingModeAddsThinkAfterLastUser) {
auto encoder = make_encoder();

Expand All @@ -85,9 +110,21 @@ TEST(DeepseekV4CppTemplate, ThinkingModeAddsThinkAfterLastUser) {
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

// An absent reasoning_effort resolves to the "high" tier, so the prefix is
// present. See ReasoningEffortHighPrefixesThinkingPrompt.
EXPECT_EQ(*prompt,
"<|begin▁of▁sentence|><|User|>Hello"
"<|Assistant|><think>");
std::string("<|begin▁of▁sentence|>") +
"Reasoning Effort: Absolute maximum with no shortcuts "
"permitted.\n"
"You MUST be very thorough in your thinking and "
"comprehensively decompose the problem to resolve the root "
"cause, rigorously stress-testing your logic against all "
"potential paths, edge cases, and adversarial scenarios.\n"
"Explicitly write out your entire deliberation process, "
"documenting every intermediate step, considered alternative, "
"and rejected hypothesis to ensure absolutely no assumption is "
"left unchecked.\n\n"
"<|User|>Hello<|Assistant|><think>");
}

TEST(DeepseekV4CppTemplate, UsesToolCallsBlockName) {
Expand Down Expand Up @@ -124,6 +161,7 @@ TEST(DeepseekV4CppTemplate, ToolResultIsMergedAsUserToolResult) {
messages.push_back(tool_msg);

nlohmann::ordered_json kwargs = nlohmann::json::object();
kwargs["thinking"] = false;
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

Expand Down Expand Up @@ -195,6 +233,7 @@ TEST(DeepseekV4CppTemplate, LatestReminderUsesDedicatedToken) {
messages.emplace_back("user", "你好");

nlohmann::ordered_json kwargs = nlohmann::json::object();
kwargs["thinking"] = false;
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

Expand All @@ -212,6 +251,7 @@ TEST(DeepseekV4CppTemplate, AdjacentUserMessagesAreMergedAsContentBlocks) {
messages.emplace_back("user", "second");

nlohmann::ordered_json kwargs = nlohmann::json::object();
kwargs["thinking"] = false;
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

Expand Down Expand Up @@ -246,22 +286,110 @@ TEST(DeepseekV4CppTemplate, ToolResultsAreSortedByToolCallId) {
EXPECT_LT(first_pos, second_pos);
}

TEST(DeepseekV4CppTemplate, ReasoningEffortMaxPrefixesThinkingPrompt) {
std::optional<std::string> render_with_effort(const std::string& effort) {
auto encoder = make_encoder();

ChatMessages messages;
messages.emplace_back("user", "hard problem");

nlohmann::ordered_json kwargs = nlohmann::json::object();
kwargs["thinking"] = true;
if (!effort.empty()) {
kwargs["reasoning_effort"] = effort;
}
return encoder.apply(messages, /*json_tools=*/{}, kwargs);
}

// DeepSeek-V4-Flash-0731 introduced a third tier above "high". Only "max"
// reaches it.
TEST(DeepseekV4CppTemplate, ReasoningEffortMaxPrefixesThinkingPrompt) {
auto prompt = render_with_effort("max");
ASSERT_TRUE(prompt.has_value());

EXPECT_NE(prompt->find("Reasoning Effort: Beyond maximum"),
std::string::npos);
EXPECT_EQ(prompt->find("Reasoning Effort: Absolute maximum"),
std::string::npos);
EXPECT_LT(prompt->find("Reasoning Effort: Beyond maximum"),
prompt->find("<|User|>hard problem"));
}

// "high" and its alias "xhigh" carry the "Absolute maximum" prefix, as does an
// absent or unrecognized value.
TEST(DeepseekV4CppTemplate, ReasoningEffortHighPrefixesThinkingPrompt) {
for (const std::string effort : {"high", "xhigh", "unexpected", ""}) {
auto prompt = render_with_effort(effort);
ASSERT_TRUE(prompt.has_value()) << "effort=" << effort;
EXPECT_NE(prompt->find("Reasoning Effort: Absolute maximum"),
std::string::npos)
<< "effort=" << effort;
EXPECT_EQ(prompt->find("Reasoning Effort: Beyond maximum"),
std::string::npos)
<< "effort=" << effort;
EXPECT_LT(prompt->find("Reasoning Effort: Absolute maximum"),
prompt->find("<|User|>hard problem"))
<< "effort=" << effort;
}
}

// "minimal", "low" and "medium" collapse to the "low" tier, which renders no
// prefix at all.
TEST(DeepseekV4CppTemplate, ReasoningEffortLowAddsNoPrefix) {
for (const std::string effort : {"minimal", "low", "medium"}) {
auto prompt = render_with_effort(effort);
ASSERT_TRUE(prompt.has_value()) << "effort=" << effort;
EXPECT_EQ(prompt->find("Reasoning Effort:"), std::string::npos)
<< "effort=" << effort;
EXPECT_NE(prompt->find("<|User|>hard problem"), std::string::npos)
<< "effort=" << effort;
}
}

// "none" disables thinking outright even though thinking=true was passed, so
// the prompt is rendered in chat mode (closing </think> instead of <think>).
TEST(DeepseekV4CppTemplate, ReasoningEffortNoneDisablesThinking) {
auto prompt = render_with_effort("none");
ASSERT_TRUE(prompt.has_value());

EXPECT_EQ(*prompt,
"<|begin▁of▁sentence|><|User|>hard problem"
"<|Assistant|></think>");
}

// A reasoning_effort on its own enables thinking, without any thinking flag.
TEST(DeepseekV4CppTemplate, ReasoningEffortAloneEnablesThinking) {
auto encoder = make_encoder();

ChatMessages messages;
messages.emplace_back("user", "hard problem");

nlohmann::ordered_json kwargs = nlohmann::json::object();
kwargs["reasoning_effort"] = "max";
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

EXPECT_NE(prompt->find("Reasoning Effort: Absolute maximum"),
EXPECT_NE(prompt->find("Reasoning Effort: Beyond maximum"),
std::string::npos);
EXPECT_LT(prompt->find("Reasoning Effort: Absolute maximum"),
prompt->find("<|User|>hard problem"));
EXPECT_NE(prompt->find("<|Assistant|><think>"), std::string::npos);
}

// An explicit thinking flag still wins over the effort, and chat mode never
// carries a prefix.
TEST(DeepseekV4CppTemplate, ExplicitThinkingFalseOverridesReasoningEffort) {
auto encoder = make_encoder();

ChatMessages messages;
messages.emplace_back("user", "hard problem");

nlohmann::ordered_json kwargs = nlohmann::json::object();
kwargs["thinking"] = false;
kwargs["reasoning_effort"] = "max";
auto prompt = encoder.apply(messages, /*json_tools=*/{}, kwargs);
ASSERT_TRUE(prompt.has_value());

EXPECT_EQ(*prompt,
"<|begin▁of▁sentence|><|User|>hard problem"
"<|Assistant|></think>");
}

} // namespace
Expand Down
112 changes: 98 additions & 14 deletions xllm/core/framework/chat_template/deepseek_v4_cpp_template.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ constexpr const char* kToolCallTemplate =
constexpr const char* kToolOutputTemplate =
"<tool_result>{content}</tool_result>";

constexpr const char* kReasoningEffortMax =
// Prompt prefixes for the reasoning effort tiers the encoder understands,
// mirroring vLLM's REASONING_EFFORT_PROMPTS. The "low" tier renders nothing.
constexpr const char* kReasoningEffortPromptHigh =
"Reasoning Effort: Absolute maximum with no "
"shortcuts permitted.\n"
"You MUST be very thorough in your thinking "
Expand All @@ -140,6 +142,30 @@ constexpr const char* kReasoningEffortMax =
"hypothesis to ensure absolutely no assumption "
"is left unchecked.\n\n";

constexpr const char* kReasoningEffortPromptMax =
"Reasoning Effort: Beyond maximum — "
"exhaustive, relentless, and "
"uncompromising.\n"
"You MUST reason with the utmost depth and "
"rigor, leaving absolutely nothing to chance: "
"exhaustively decompose the problem into its "
"most fundamental components, trace every "
"causal chain to its root, and resolve the "
"underlying cause rather than any surface "
"symptom.\n"
"Do not stop reasoning until you have "
"independently verified the solution from "
"multiple angles and are certain that no "
"assumption remains unchecked and no error "
"remains undiscovered.\n\n";

// Reasoning effort tiers understood by the prompt encoder. Request-level values
// are collapsed onto these by resolve_reasoning_effort().
constexpr const char* kReasoningEffortNone = "none";
constexpr const char* kReasoningEffortLowTier = "low";
constexpr const char* kReasoningEffortHighTier = "high";
constexpr const char* kReasoningEffortMaxTier = "max";

constexpr const char* kThinkingModeThinking = "thinking";
constexpr const char* kThinkingModeChat = "chat";

Expand Down Expand Up @@ -199,14 +225,6 @@ bool get_thinking_enabled(const nlohmann::ordered_json& kwargs) {
return false;
}

std::string get_thinking_mode(const nlohmann::ordered_json& kwargs) {
if (kwargs.contains("thinking_mode") && kwargs["thinking_mode"].is_string()) {
return kwargs["thinking_mode"].get<std::string>();
}
return get_thinking_enabled(kwargs) ? kThinkingModeThinking
: kThinkingModeChat;
}

std::string get_reasoning_effort(const nlohmann::ordered_json& kwargs) {
if (kwargs.contains("reasoning_effort") &&
kwargs["reasoning_effort"].is_string()) {
Expand All @@ -215,6 +233,71 @@ std::string get_reasoning_effort(const nlohmann::ordered_json& kwargs) {
return "";
}

// Collapse a request-level reasoning effort onto the tiers the prompt encoder
// understands. DeepSeek-V4-Flash-0731 widened the accepted set to
// none/minimal/low/medium/high/xhigh/max; this mirrors the mapping in vLLM's
// DeepSeek-V4 tokenizer wrapper and Rust renderer:
// "none" -> "" (and forces chat mode, see
// get_thinking_mode)
// "minimal" / "low" / "medium" -> "low" (renders no prefix)
// "max" -> "max"
// "high" / "xhigh" / absent /
// anything unrecognized -> "high"
std::string resolve_reasoning_effort(const std::string& reasoning_effort) {
if (reasoning_effort == kReasoningEffortNone) {
return "";
}
if (reasoning_effort == kReasoningEffortMaxTier) {
return kReasoningEffortMaxTier;
}
if (reasoning_effort == "minimal" ||
reasoning_effort == kReasoningEffortLowTier ||
reasoning_effort == "medium") {
return kReasoningEffortLowTier;
}
return kReasoningEffortHighTier;
}

// Prefix for an already-resolved tier. Unknown tiers degrade to "low", matching
// vLLM's DEFAULT_REASONING_EFFORT.
const char* reasoning_effort_prompt(const std::string& resolved_effort) {
if (resolved_effort == kReasoningEffortMaxTier) {
return kReasoningEffortPromptMax;
}
if (resolved_effort == kReasoningEffortHighTier) {
return kReasoningEffortPromptHigh;
}
return "";
}

bool has_explicit_thinking_flag(const nlohmann::ordered_json& kwargs) {
return (kwargs.contains("thinking") && kwargs["thinking"].is_boolean()) ||
(kwargs.contains("enable_thinking") &&
kwargs["enable_thinking"].is_boolean());
}

std::string get_thinking_mode(const nlohmann::ordered_json& kwargs) {
if (kwargs.contains("thinking_mode") && kwargs["thinking_mode"].is_string()) {
return kwargs["thinking_mode"].get<std::string>();
}
const std::string reasoning_effort = get_reasoning_effort(kwargs);
// reasoning_effort="none" disables thinking outright, even when a thinking
// flag asks for it. Matches vLLM's DeepSeek-V4 tokenizer wrapper.
if (reasoning_effort == kReasoningEffortNone) {
return kThinkingModeChat;
}
// Without an explicit thinking flag, thinking is on: either implied by a
// reasoning_effort, or by DeepSeek-V4's own default. Matches vLLM's
// DeepSeek-V4 tokenizer wrapper, which sets thinking_enabled = true when
// neither "thinking" nor "enable_thinking" is present, and xLLM's own
// get_enable_thinking_from_request() default in chat_service_impl.cpp.
if (!has_explicit_thinking_flag(kwargs)) {
return kThinkingModeThinking;
}
return get_thinking_enabled(kwargs) ? kThinkingModeThinking
: kThinkingModeChat;
}

std::vector<nlohmann::ordered_json> tool_calls_from_openai_format(
const Message::ToolCallVec& tool_calls) {
std::vector<nlohmann::ordered_json> out;
Expand Down Expand Up @@ -525,10 +608,10 @@ std::string render_message(const nlohmann::ordered_json& messages,
response_format = msg["response_format"];
}

// Reasoning effort prefix at index 0
if (index == 0 && thinking_mode == kThinkingModeThinking &&
reasoning_effort == "max") {
prompt += kReasoningEffortMax;
// Reasoning effort prefix at index 0. reasoning_effort is already collapsed
// to a tier by resolve_reasoning_effort(); the "low" tier renders nothing.
if (index == 0 && thinking_mode == kThinkingModeThinking) {
prompt += reasoning_effort_prompt(reasoning_effort);
}

if (role == kRoleSystem) {
Expand Down Expand Up @@ -691,7 +774,8 @@ std::optional<std::string> DeepseekV4CppTemplate::apply(
nlohmann::ordered_json normalized =
normalize_messages(messages, json_tools);
std::string thinking_mode = get_thinking_mode(chat_template_kwargs);
std::string reasoning_effort = get_reasoning_effort(chat_template_kwargs);
std::string reasoning_effort =
resolve_reasoning_effort(get_reasoning_effort(chat_template_kwargs));

// Preprocess: merge tool + sort
normalized = merge_tool_messages(normalized);
Expand Down
Loading