diff --git a/server/docs/DS4.md b/server/docs/DS4.md index 3de39b571..6687332da 100644 --- a/server/docs/DS4.md +++ b/server/docs/DS4.md @@ -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 | diff --git a/server/src/server/chat_template.cpp b/server/src/server/chat_template.cpp index 2d5970efa..b9cf36eff 100644 --- a/server/src/server/chat_template.cpp +++ b/server/src/server/chat_template.cpp @@ -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"; @@ -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. " diff --git a/server/src/server/chat_template.h b/server/src/server/chat_template.h index ecade9217..f93119906 100644 --- a/server/src/server/chat_template.h +++ b/server/src/server/chat_template.h @@ -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 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 & 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); diff --git a/server/src/server/http_server.cpp b/server/src/server/http_server.cpp index 9d6f9f12d..324a1bee3 100644 --- a/server/src/server/http_server.cpp +++ b/server/src/server/http_server.cpp @@ -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; @@ -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 = { @@ -1786,10 +1791,13 @@ 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; } @@ -1797,12 +1805,18 @@ void HttpServer::apply_request_reasoning( 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; @@ -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()); + } + if (kwargs.contains("thinking") && kwargs["thinking"].is_boolean()) { + enable_thinking = kwargs["thinking"].get(); + req.thinking_opt_in = enable_thinking; + } if (kwargs.contains("enable_thinking")) { enable_thinking = kwargs["enable_thinking"].get(); + 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 @@ -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 @@ -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); @@ -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()); } @@ -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(); diff --git a/server/src/server/http_server.h b/server/src/server/http_server.h index 9e14e8b1f..2702f23a0 100644 --- a/server/src/server/http_server.h +++ b/server/src/server/http_server.h @@ -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 diff --git a/server/test/test_server_unit.cpp b/server/test/test_server_unit.cpp index 8373859dc..481e3cf44 100644 --- a/server/test/test_server_unit.cpp +++ b/server/test/test_server_unit.cpp @@ -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 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|>")); + + 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|>")); + + 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|>")); +} + TEST_CASE(ServerUnitFixture, test_jinja_render_basic) { std::vector msgs = { {"system", "you are helpful", ""}, @@ -4787,6 +4827,20 @@ TEST_CASE(ServerUnitFixture, test_props_model_card_null_on_family_fallback) { TEST_ASSERT(body["budget_envelope"]["default_max_tokens"].get() == 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()); + TEST_ASSERT(body["reasoning"]["supported_efforts"] == + json::array({"low", "high", "max"})); + TEST_ASSERT(body["capabilities"]["reasoning_supported"].get()); +} + 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).