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
9 changes: 9 additions & 0 deletions server/docs/DS4.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ 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 |

## Tool-call compatibility

The native DeepSeek V4 renderer requests `<function_call>` output. The parser
also accepts named bare-JSON fallbacks emitted by compatible checkpoints:
`{"function":"name","parameters":{...}}` and the legacy OpenAI
`{"function_call":{"name":"name","arguments":{...}}}` envelope. Named JSON
calls remain unambiguous when a request supplies more than one tool; ordinary
JSON that does not resolve to an allowed tool is preserved as assistant text.

## Code Layout

| Area | Files |
Expand Down
2 changes: 1 addition & 1 deletion server/src/server/http_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ json build_props_body(const ServerConfig & config,
const bool is_qwen = (config.arch.rfind("qwen", 0) == 0);
const bool reasoning_supported = is_qwen;
const bool speculative_supported = is_qwen;
const bool tools_supported = is_qwen;
const bool tools_supported = is_qwen || config.arch == "deepseek4";

auto pcs = prefix_cache.stats();
auto pcfs = prefix_cache.full_stats();
Expand Down
6 changes: 1 addition & 5 deletions server/src/server/sse_emitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,9 @@ static bool has_request_tools(const json & tools) {
return tools.is_array() && !tools.empty();
}

static bool has_single_request_tool(const json & tools) {
return tools.is_array() && tools.size() == 1 && tools[0].is_object();
}

static bool starts_with_potential_bare_json_tool(const std::string & text,
const json & tools) {
if (!has_single_request_tool(tools)) return false;
if (!has_request_tools(tools)) return false;
size_t first = text.find_first_not_of(" \t\n\r");
return first != std::string::npos && text[first] == '{';
}
Expand Down
23 changes: 20 additions & 3 deletions server/src/server/tool_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ static bool parse_complete_parameter_body(const std::string & body,

// ─── JSON tool call parser ──────────────────────────────────────────────

// Parse {"name": ..., "arguments": ...} or {"function": {"name": ..., "arguments": ...}}
// Parse the named JSON tool-call envelopes emitted by supported chat models.
static bool parse_json_tool_call(const json & obj, std::string & out_name, json & out_args) {
if (!obj.is_object()) return false;

Expand Down Expand Up @@ -588,8 +588,25 @@ static bool parse_json_tool_call(const json & obj, std::string & out_name, json
return false;
}
}
} else if (obj.contains("function") && obj["function"].is_object()) {
const auto & fn = obj["function"];
} else if (obj.contains("function") && obj["function"].is_string()) {
name = obj["function"].get<std::string>();
if (!obj.contains("parameters")) {
return false;
}
if (obj["parameters"].is_object()) {
args = obj["parameters"];
} else if (obj["parameters"].is_string()) {
try { args = json::parse(obj["parameters"].get<std::string>()); }
catch (...) { return false; }
} else {
return false;
}
} else if ((obj.contains("function") && obj["function"].is_object()) ||
(obj.contains("function_call") &&
obj["function_call"].is_object())) {
const auto & fn = obj.contains("function")
? obj["function"]
: obj["function_call"];
if (!fn.contains("name") || !fn["name"].is_string()) return false;
name = fn["name"].get<std::string>();
if (fn.contains("arguments")) {
Expand Down
86 changes: 86 additions & 0 deletions server/test/test_server_unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,51 @@ TEST_CASE(ServerUnitFixture, test_parse_function_call_wrapper) {
}
}

TEST_CASE(ServerUnitFixture, test_parse_legacy_openai_function_call_json) {
const std::string text =
"{\"function_call\":{\"arguments\":"
"\"{\\\"location\\\":\\\"test-city\\\"}\","
"\"name\":\"get_weather\"}}";
const auto result = parse_tool_calls(text, weather_tools());
TEST_ASSERT(result.tool_calls.size() == 1);
if (!result.tool_calls.empty()) {
TEST_ASSERT(result.tool_calls[0].name == "get_weather");
const auto args = json::parse(result.tool_calls[0].arguments);
TEST_ASSERT(args["location"] == "test-city");
}
TEST_ASSERT(result.cleaned_text.empty());
}

TEST_CASE(ServerUnitFixture, test_parse_deepseek_function_parameters_json) {
const std::string text =
"{\"function\":\"get_weather\",\"parameters\":{"
"\"location\":\"test-city\",\"unit\":\"celsius\"}}";
const auto result = parse_tool_calls(text, weather_tools());
TEST_ASSERT(result.tool_calls.size() == 1);
if (!result.tool_calls.empty()) {
TEST_ASSERT(result.tool_calls[0].name == "get_weather");
const auto args = json::parse(result.tool_calls[0].arguments);
TEST_ASSERT(args["location"] == "test-city");
TEST_ASSERT(args["unit"] == "celsius");
}
TEST_ASSERT(result.cleaned_text.empty());
}

TEST_CASE(ServerUnitFixture, test_parse_deepseek_function_stringified_parameters_json) {
const std::string text =
"{\"function\":\"get_weather\",\"parameters\":"
"\"{\\\"location\\\":\\\"test-city\\\",\\\"unit\\\":\\\"celsius\\\"}\"}";
const auto result = parse_tool_calls(text, weather_tools());
TEST_ASSERT(result.tool_calls.size() == 1);
if (!result.tool_calls.empty()) {
TEST_ASSERT(result.tool_calls[0].name == "get_weather");
const auto args = json::parse(result.tool_calls[0].arguments);
TEST_ASSERT(args["location"] == "test-city");
TEST_ASSERT(args["unit"] == "celsius");
}
TEST_ASSERT(result.cleaned_text.empty());
}

TEST_CASE(ServerUnitFixture, test_parse_bare_function_json_with_parameters) {
std::string text =
"<function>\n"
Expand Down Expand Up @@ -1440,6 +1485,36 @@ TEST_CASE(ServerUnitFixture, test_emitter_bare_function_json_tool_buffer_detecti
TEST_ASSERT(em.accumulated_text().find("bash") == std::string::npos);
}

TEST_CASE(ServerUnitFixture, test_emitter_named_json_with_multiple_tools) {
auto em = make_emitter(ApiFormat::OPENAI_CHAT, read_and_bash_tools());
em.emit_start();
em.emit_token("{\"function\":\"bash\",");
em.emit_token("\"parameters\":{\"command\":\"pwd\"}}");
const auto finish = em.emit_finish(20);

TEST_ASSERT(em.tool_calls().size() == 1);
if (!em.tool_calls().empty()) {
TEST_ASSERT(em.tool_calls()[0].name == "bash");
const auto args = json::parse(em.tool_calls()[0].arguments);
TEST_ASSERT(args["command"] == "pwd");
}
TEST_ASSERT(em.accumulated_text().empty());
const std::string wire = concat(finish);
TEST_ASSERT(wire.find("bash") != std::string::npos);
TEST_ASSERT(wire.find("tool_calls") != std::string::npos);
}

TEST_CASE(ServerUnitFixture, test_emitter_multi_tool_json_content_is_preserved) {
auto em = make_emitter(ApiFormat::OPENAI_CHAT, read_and_bash_tools());
em.emit_start();
em.emit_token("{\"status\":\"ok\"}");
const auto finish = em.emit_finish(20);

TEST_ASSERT(em.tool_calls().empty());
TEST_ASSERT(em.accumulated_text() == "{\"status\":\"ok\"}");
TEST_ASSERT(concat(finish).find("status") != std::string::npos);
}



TEST_CASE(ServerUnitFixture, test_emitter_anthropic_tool_use_blocks) {
Expand Down Expand Up @@ -4787,6 +4862,17 @@ 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_tool_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["capabilities"]["tools_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