fix(server): accept DeepSeek named JSON tool calls - #631
Conversation
There was a problem hiding this comment.
2 issues found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/src/server/tool_parser.cpp">
<violation number="1" location="server/src/server/tool_parser.cpp:593">
P2: The new DeepSeek string-`function` branch only accepts `parameters` when it is a JSON object and returns false otherwise. This is inconsistent with every sibling branch in `parse_json_tool_call` (the `name`+`arguments` branch and the `function`/`function_call` object-envelope branch), which explicitly accept `arguments`/`parameters` as a JSON string and parse it via `json::parse`. A DeepSeek-compatible emitted tool call that stringifies its arguments, e.g. `{"function":"get_weather","parameters":"{\"location\":\"Prague\"}"}`, is silently rejected and dropped (for a whole-response JSON, it falls back to assistant content), while the equivalent legacy `{"function_call":{"name":...,"arguments":"{...}"}}` form is handled and is covered by `test_parse_legacy_openai_function_call_json`. Mirror the string-parse logic for consistency and robustness.</violation>
</file>
<file name="server/test/test_server_unit.cpp">
<violation number="1" location="server/test/test_server_unit.cpp:4858">
P2: This assertion reads `body["tools"]["supported"]`, but `build_props_body` (http_server.cpp:655-882) never emits a `tools` key — tool capability is advertised only under `capabilities.tools_supported`. Because `body` is `const json`, the const `operator[]` on the missing `tools` key throws `json::out_of_range` (and even if it yielded a null reference, `.get<bool>()` on null would throw), so this test throws instead of asserting anything. Keep only the `capabilities.tools_supported` check, which is the real advertised field.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const auto & fn = obj["function"]; | ||
| } else if (obj.contains("function") && obj["function"].is_string()) { | ||
| name = obj["function"].get<std::string>(); | ||
| if (!obj.contains("parameters") || !obj["parameters"].is_object()) { |
There was a problem hiding this comment.
P2: The new DeepSeek string-function branch only accepts parameters when it is a JSON object and returns false otherwise. This is inconsistent with every sibling branch in parse_json_tool_call (the name+arguments branch and the function/function_call object-envelope branch), which explicitly accept arguments/parameters as a JSON string and parse it via json::parse. A DeepSeek-compatible emitted tool call that stringifies its arguments, e.g. {"function":"get_weather","parameters":"{\"location\":\"Prague\"}"}, is silently rejected and dropped (for a whole-response JSON, it falls back to assistant content), while the equivalent legacy {"function_call":{"name":...,"arguments":"{...}"}} form is handled and is covered by test_parse_legacy_openai_function_call_json. Mirror the string-parse logic for consistency and robustness.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/tool_parser.cpp, line 593:
<comment>The new DeepSeek string-`function` branch only accepts `parameters` when it is a JSON object and returns false otherwise. This is inconsistent with every sibling branch in `parse_json_tool_call` (the `name`+`arguments` branch and the `function`/`function_call` object-envelope branch), which explicitly accept `arguments`/`parameters` as a JSON string and parse it via `json::parse`. A DeepSeek-compatible emitted tool call that stringifies its arguments, e.g. `{"function":"get_weather","parameters":"{\"location\":\"Prague\"}"}`, is silently rejected and dropped (for a whole-response JSON, it falls back to assistant content), while the equivalent legacy `{"function_call":{"name":...,"arguments":"{...}"}}` form is handled and is covered by `test_parse_legacy_openai_function_call_json`. Mirror the string-parse logic for consistency and robustness.</comment>
<file context>
@@ -588,8 +588,18 @@ static bool parse_json_tool_call(const json & obj, std::string & out_name, json
- const auto & fn = obj["function"];
+ } else if (obj.contains("function") && obj["function"].is_string()) {
+ name = obj["function"].get<std::string>();
+ if (!obj.contains("parameters") || !obj["parameters"].is_object()) {
+ return false;
+ }
</file context>
| ToolMemory tm; | ||
| const json body = build_props_body(cfg, pc, tm); | ||
|
|
||
| TEST_ASSERT(body["tools"]["supported"].get<bool>()); |
There was a problem hiding this comment.
P2: This assertion reads body["tools"]["supported"], but build_props_body (http_server.cpp:655-882) never emits a tools key — tool capability is advertised only under capabilities.tools_supported. Because body is const json, the const operator[] on the missing tools key throws json::out_of_range (and even if it yielded a null reference, .get<bool>() on null would throw), so this test throws instead of asserting anything. Keep only the capabilities.tools_supported check, which is the real advertised field.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/test/test_server_unit.cpp, line 4858:
<comment>This assertion reads `body["tools"]["supported"]`, but `build_props_body` (http_server.cpp:655-882) never emits a `tools` key — tool capability is advertised only under `capabilities.tools_supported`. Because `body` is `const json`, the const `operator[]` on the missing `tools` key throws `json::out_of_range` (and even if it yielded a null reference, `.get<bool>()` on null would throw), so this test throws instead of asserting anything. Keep only the `capabilities.tools_supported` check, which is the real advertised field.</comment>
<file context>
@@ -4787,6 +4847,18 @@ TEST_CASE(ServerUnitFixture, test_props_model_card_null_on_family_fallback) {
+ ToolMemory tm;
+ const json body = build_props_body(cfg, pc, tm);
+
+ TEST_ASSERT(body["tools"]["supported"].get<bool>());
+ TEST_ASSERT(body["capabilities"]["tools_supported"].get<bool>());
+}
</file context>
| TEST_ASSERT(body["tools"]["supported"].get<bool>()); | |
| TEST_ASSERT(body["capabilities"]["tools_supported"].get<bool>()); |
Summary
Accept the named bare-JSON tool-call fallbacks emitted by DeepSeek-compatible checkpoints:
{"function":"get_weather","parameters":{"location":"Prague"}}and the legacy envelope:
{"function_call":{"name":"get_weather","arguments":{"location":"Prague"}}}The streaming emitter now buffers a leading JSON object when multiple tools are declared. Final parsing still requires an allowed tool name; an ordinary JSON response that does not resolve to an allowed tool is returned unchanged as assistant content. DeepSeek V4 tool capability is advertised through
/props.Published qualification patches:
lucebox-deepseek-function-call.patchlucebox-deepseek-function-parameters.patchlucebox-multi-tool-json-buffer.patchValidation
/propsunit coveragegit diff --checkThis complements #627: that PR handles
<function_calls>XML emitted inside reasoning, while this change handles named bare-JSON fallback forms.