Skip to content
Open
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
15 changes: 15 additions & 0 deletions server/docs/DS4.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ DeepSeek V4 Flash is a 43-layer MoE model with:
| Indexer | Top-k scorer on ratio-4 layers for compressed KV selection |
| HC (Hierarchical Controller) | 4 parallel residual streams, Sinkhorn-normalized combine |

## Reasoning effort

The native DeepSeek V4 renderer supports the official `low`, `high`, and `max`
reasoning encodings. `low` adds no prefix; `high` and `max` prepend their
official model-facing instruction before the system message. The server accepts
`reasoning.effort`, top-level `reasoning_effort`, and the official
`chat_template_kwargs` form:

```json
{"chat_template_kwargs":{"thinking":true,"reasoning_effort":"max"}}
```

For client compatibility, `medium` maps to `high`, while `x-high` and `xhigh`
map to `max`. Disabling thinking suppresses every effort prefix.

## Code Layout

| Area | Files |
Expand Down
12 changes: 11 additions & 1 deletion server/src/server/chat_template.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ std::string render_chat_template(
ChatFormat format,
bool add_generation_prompt,
bool enable_thinking,
const std::string & tools_json)
const std::string & tools_json,
const std::string & reasoning_effort)
{
std::string result;
bool has_tools = !tools_json.empty() && tools_json != "[]" && tools_json != "null";
Expand Down Expand Up @@ -375,6 +376,15 @@ std::string render_chat_template(
}

result = "<|begin▁of▁sentence|>";
if (enable_thinking && reasoning_effort == "high") {
result += "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n";
result += "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";
result += "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";
} else if (enable_thinking && reasoning_effort == "max") {
result += "Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\n";
result += "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";
result += "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";
}
if (has_tools) {
result += "### Tools\n\n"
"You may call functions to assist with the user query. "
Expand Down
6 changes: 5 additions & 1 deletion server/src/server/chat_template.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ enum class ChatFormat {
// `tools_json` is an optional JSON string containing the tool definitions
// array. When non-empty, the Qwen3/3.5 template injects a tool preamble
// into the system message instructing the model how to emit <tool_call> tags.
//
// `reasoning_effort` is the normalized model-facing effort. DeepSeek V4 uses
// low, high, and max; high and max prepend the official encoding prefixes.
std::string render_chat_template(
const std::vector<ChatMessage> & messages,
ChatFormat format,
bool add_generation_prompt = true,
bool enable_thinking = false,
const std::string & tools_json = "");
const std::string & tools_json = "",
const std::string & reasoning_effort = "");

// Detect the appropriate chat format for an architecture.
ChatFormat chat_format_for_arch(const std::string & arch);
Expand Down
43 changes: 36 additions & 7 deletions server/src/server/http_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,8 @@ json build_props_body(const ServerConfig & config,
const ToolMemory & tool_memory) {
// arch-gated capabilities (mirrors Python _capabilities()).
const bool is_qwen = (config.arch.rfind("qwen", 0) == 0);
const bool reasoning_supported = is_qwen;
const bool is_deepseek4 = (config.arch == "deepseek4");
const bool reasoning_supported = is_qwen || is_deepseek4;
const bool speculative_supported = is_qwen;
const bool tools_supported = is_qwen;

Expand All @@ -687,12 +688,16 @@ json build_props_body(const ServerConfig & config,
// all activate the phase-1 envelope. Advertise the full set when the
// arch supports reasoning so clients can negotiate the higher tiers.
json reasoning_efforts = json::array();
if (reasoning_supported) {
if (is_qwen) {
reasoning_efforts.push_back("low");
reasoning_efforts.push_back("medium");
reasoning_efforts.push_back("high");
reasoning_efforts.push_back("x-high");
reasoning_efforts.push_back("max");
} else if (is_deepseek4) {
reasoning_efforts.push_back("low");
reasoning_efforts.push_back("high");
reasoning_efforts.push_back("max");
}

json server = {
Expand Down Expand Up @@ -1786,23 +1791,32 @@ void HttpServer::apply_request_reasoning(
int request_reply_budget = -1;
int effort_phase1_cap = -1;
bool effort_set = false;
std::string normalized_effort;

auto apply_reasoning_effort = [&](const std::string & effort) {
if (effort == "none") {
enable_thinking = false;
normalized_effort.clear();
effort_set = true;
return;
}

// Five-tier vocabulary (spec §4.2). Unknown tier → high.
int tier_value = config_.effort_tiers.high;
if (effort == "minimal" || effort == "low") {
tier_value = config_.effort_tiers.low;
normalized_effort = "low";
} else if (effort == "medium") {
tier_value = config_.effort_tiers.medium;
} else if (effort == "x-high") {
normalized_effort = config_.arch == "deepseek4" ? "high" : "medium";
} else if (effort == "x-high" || effort == "xhigh") {
tier_value = config_.effort_tiers.x_high;
normalized_effort = config_.arch == "deepseek4" ? "max" : "x-high";
} else if (effort == "max") {
tier_value = config_.effort_tiers.max;
normalized_effort = "max";
} else {
normalized_effort = "high";
}

effort_phase1_cap = tier_value;
Expand Down Expand Up @@ -1842,11 +1856,23 @@ void HttpServer::apply_request_reasoning(
}
if (body.contains("chat_template_kwargs")) {
const auto & kwargs = body["chat_template_kwargs"];
if (!effort_set && kwargs.contains("reasoning_effort") &&
kwargs["reasoning_effort"].is_string()) {
apply_reasoning_effort(
kwargs["reasoning_effort"].get<std::string>());
}
if (kwargs.contains("thinking") && kwargs["thinking"].is_boolean()) {
enable_thinking = kwargs["thinking"].get<bool>();
req.thinking_opt_in = enable_thinking;
}
if (kwargs.contains("enable_thinking")) {
enable_thinking = kwargs["enable_thinking"].get<bool>();
req.thinking_opt_in = enable_thinking;
}
}
if (!enable_thinking) normalized_effort.clear();
req.thinking_enabled = enable_thinking;
req.reasoning_effort = normalized_effort;

// Spec §4.3 combined precedence + §4.4 clamping: thinking.budget_tokens
// (if set) wins over reasoning.effort for the phase-1 cap; either is
Expand All @@ -1860,7 +1886,7 @@ void HttpServer::apply_request_reasoning(
"think_max_tokens=%d\n",
request_budget_tokens, config_.think_max_tokens);
}
} else if (effort_set) {
} else if (effort_set && enable_thinking) {
// Spec §4.4: effective cap is min(tier value, max_tokens -
// hard_limit_reply_budget). Tier values can legitimately exceed
// default_max_tokens; clients that want the full tier budget must
Expand Down Expand Up @@ -1929,7 +1955,7 @@ bool HttpServer::render_and_tokenize_request(
} else {
rendered = render_chat_template(
chat_messages, chat_format_, /*add_generation_prompt=*/true,
req.thinking_enabled, tools_json);
req.thinking_enabled, tools_json, req.reasoning_effort);
}

req.started_in_thinking = prompt_ends_in_open_think(rendered);
Expand Down Expand Up @@ -1960,12 +1986,15 @@ bool HttpServer::validate_request_context(
void HttpServer::log_parsed_request(const ParsedRequest & req) const {
std::fprintf(stderr,
"[server] chat %s format=%s stream=%s msgs=%zu tools=%zu prompt_tokens=%zu "
"max_tokens=%d max_ctx=%d thinking=%s started_in_thinking=%s stops=%zu model=%s\n",
"max_tokens=%d max_ctx=%d thinking=%s reasoning_effort=%s "
"started_in_thinking=%s stops=%zu model=%s\n",
req.response_id.c_str(), api_format_name(req.format),
req.stream ? "true" : "false",
json_array_size(req.messages), json_array_size(req.tools),
req.prompt_tokens.size(), req.max_output, config_.max_ctx,
req.thinking_enabled ? "true" : "false",
!req.thinking_enabled ? "none" :
(req.reasoning_effort.empty() ? "low" : req.reasoning_effort.c_str()),
req.started_in_thinking ? "true" : "false",
req.stop_sequences.size(), req.model.c_str());
}
Expand Down Expand Up @@ -2674,7 +2703,7 @@ void HttpServer::apply_flowkv_compression(
} else {
rendered = render_chat_template(
chat_messages, chat_format_, /*add_generation_prompt=*/true,
req.thinking_enabled, tools_json);
req.thinking_enabled, tools_json, req.reasoning_effort);
}

const int tokens_before = (int) prepared.tokens.size();
Expand Down
3 changes: 3 additions & 0 deletions server/src/server/http_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ struct ParsedRequest {
// Thinking/reasoning state
bool thinking_enabled = true;
bool started_in_thinking = false;
// Normalized model-facing effort. DeepSeek V4 officially defines low,
// high, and max; high and max select distinct prompt prefixes.
std::string reasoning_effort;
// True when the request opted in to the thinking-budget envelope via
// `thinking: {type: "enabled"}`. Distinct from thinking_enabled (which
// can be set via the chat template kwarg alone). When true, the response
Expand Down
54 changes: 54 additions & 0 deletions server/test/test_server_unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2743,6 +2743,46 @@ TEST_CASE(ServerUnitFixture, test_deepseek4_render_empty_chat_gen_prompt) {
TEST_ASSERT(out == expected);
}

TEST_CASE(ServerUnitFixture, test_deepseek4_render_reasoning_effort_prefixes) {
std::vector<ChatMessage> msgs = {
{"system", "system message", ""},
{"user", "hard problem", ""},
};
const std::string bos = "<|begin▁of▁sentence|>";
const std::string high_prefix =
"Reasoning Effort: Absolute maximum with no shortcuts permitted.\n";
const std::string max_prefix =
"Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\n";
const auto ends_with = [](const std::string & text,
const std::string & suffix) {
return text.size() >= suffix.size() &&
text.compare(text.size() - suffix.size(), suffix.size(), suffix) == 0;
};

const std::string high = render_chat_template(
msgs, ChatFormat::DEEPSEEK4, true, true, "", "high");
TEST_ASSERT(high.rfind(bos + high_prefix, 0) == 0);
TEST_ASSERT(high.find(max_prefix) == std::string::npos);
TEST_ASSERT(ends_with(high, "<|Assistant|><think>"));

const std::string max = render_chat_template(
msgs, ChatFormat::DEEPSEEK4, true, true, "", "max");
TEST_ASSERT(max.rfind(bos + max_prefix, 0) == 0);
TEST_ASSERT(max.find(high_prefix) == std::string::npos);
TEST_ASSERT(ends_with(max, "<|Assistant|><think>"));

const std::string low = render_chat_template(
msgs, ChatFormat::DEEPSEEK4, true, true, "", "low");
TEST_ASSERT(low.find(high_prefix) == std::string::npos);
TEST_ASSERT(low.find(max_prefix) == std::string::npos);
TEST_ASSERT(low.rfind(bos + "system message", 0) == 0);

const std::string disabled = render_chat_template(
msgs, ChatFormat::DEEPSEEK4, true, false, "", "max");
TEST_ASSERT(disabled.find(max_prefix) == std::string::npos);
TEST_ASSERT(ends_with(disabled, "<|Assistant|></think>"));
}

TEST_CASE(ServerUnitFixture, test_jinja_render_basic) {
std::vector<ChatMessage> msgs = {
{"system", "you are helpful", ""},
Expand Down Expand Up @@ -4787,6 +4827,20 @@ TEST_CASE(ServerUnitFixture, test_props_model_card_null_on_family_fallback) {
TEST_ASSERT(body["budget_envelope"]["default_max_tokens"].get<int>() == 32768);
}

TEST_CASE(ServerUnitFixture, test_props_deepseek4_reasoning_capability) {
ServerConfig cfg;
cfg.arch = "deepseek4";
Tokenizer tok;
PrefixCache pc(0, tok);
ToolMemory tm;
const json body = build_props_body(cfg, pc, tm);

TEST_ASSERT(body["reasoning"]["supported"].get<bool>());
TEST_ASSERT(body["reasoning"]["supported_efforts"] ==
json::array({"low", "high", "max"}));
TEST_ASSERT(body["capabilities"]["reasoning_supported"].get<bool>());
}

TEST_CASE(ServerUnitFixture, test_props_budget_envelope_shape) {
// budget_envelope is always present with all five fields and the
// expected effort_tiers vocabulary (low|medium|high|x-high|max).
Expand Down