Skip to content

fix(server): accept DeepSeek named JSON tool calls - #631

Open
pepuscz wants to merge 1 commit into
Luce-Org:mainfrom
pepuscz:fix/deepseek4-tool-json
Open

fix(server): accept DeepSeek named JSON tool calls#631
pepuscz wants to merge 1 commit into
Luce-Org:mainfrom
pepuscz:fix/deepseek4-tool-json

Conversation

@pepuscz

@pepuscz pepuscz commented Aug 18, 2026

Copy link
Copy Markdown

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:

Validation

  • Added parser, multi-tool streaming, ordinary-JSON fallback, and /props unit coverage
  • Compiled the current-main parser/emitter locally and ran focused named-call, legacy-call, multi-tool, and non-tool JSON assertions
  • git diff --check

This complements #627: that PR handles <function_calls> XML emitted inside reasoning, while this change handles named bare-JSON fallback forms.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
TEST_ASSERT(body["tools"]["supported"].get<bool>());
TEST_ASSERT(body["capabilities"]["tools_supported"].get<bool>());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant