char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aldehir Rojascommon : implement common_schema internal representation for JSON schemas (#28736)acecd5603

char/aliceai-t5-moe
11.6 KiB267 linesraw
1#include "parsers.h"
2
3// The DeepSeek V4 reference implementation renders consecutive tool results into a single
4// user block, ordered by the tool call order of the preceding assistant message (matched
5// by tool call id) rather than by the order they appear in the conversation.
6static json deepseek_v4_sort_tool_results(const json & messages) {
7    json adjusted = messages;
8    std::map<std::string, size_t> call_order;
9
10    for (size_t i = 0; i < adjusted.size();) {
11        const auto & msg  = adjusted[i];
12        const auto   role = msg.value("role", "");
13
14        if (role == "assistant" && msg.contains("tool_calls") &&
15                msg.at("tool_calls").is_array() && !msg.at("tool_calls").empty()) {
16            call_order.clear();
17            const auto & tool_calls = msg.at("tool_calls");
18            for (size_t idx = 0; idx < tool_calls.size(); idx++) {
19                auto id = tool_calls[idx].value("id", "");
20                if (!id.empty()) {
21                    call_order[id] = idx;
22                }
23            }
24            i++;
25            continue;
26        }
27
28        if (role != "user" && role != "tool") {
29            i++;
30            continue;
31        }
32
33        // collect a maximal run of user/tool messages - they render into one user block
34        std::vector<size_t> tool_positions;
35        size_t run_end = i;
36        for (; run_end < adjusted.size(); run_end++) {
37            const auto r = adjusted[run_end].value("role", "");
38            if (r == "tool") {
39                tool_positions.push_back(run_end);
40            } else if (r != "user") {
41                break;
42            }
43        }
44
45        if (tool_positions.size() > 1 && !call_order.empty()) {
46            std::vector<json> results;
47            results.reserve(tool_positions.size());
48            for (auto pos : tool_positions) {
49                results.push_back(adjusted[pos]);
50            }
51            std::stable_sort(results.begin(), results.end(), [&](const json & a, const json & b) {
52                const auto order = [&](const json & m) {
53                    auto it = call_order.find(m.value("tool_call_id", ""));
54                    return it == call_order.end() ? (size_t) 0 : it->second;
55                };
56                return order(a) < order(b);
57            });
58            for (size_t k = 0; k < tool_positions.size(); k++) {
59                adjusted[tool_positions[k]] = std::move(results[k]);
60            }
61        }
62
63        i = run_end;
64    }
65
66    return adjusted;
67}
68
69common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_template &    tmpl,
70                                                                 const autoparser::generation_params & inputs) {
71    common_chat_params data;
72
73    // V4 uses the same DSML markup as V3.2, but names the tool call block "tool_calls"
74    // instead of "function_calls", renders tool results in tool call order and its
75    // non-thinking generation prompt ends with a bare </think> instead of an empty
76    // <think></think> pair.
77    const bool is_v4 = tmpl.source().find("function_calls") == std::string::npos;
78
79    std::optional<json> adjusted_messages;
80    if (is_v4) {
81        adjusted_messages = deepseek_v4_sort_tool_results(inputs.messages);
82    }
83
84    auto has_tools           = inputs.tools.is_array() && !inputs.tools.empty();
85    auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
86    auto extract_reasoning   = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
87    auto include_grammar     = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
88
89    std::optional<json> additional_context;
90    if (is_v4 && has_response_format) {
91        additional_context = json{ { "response_format", inputs.json_schema } };
92    }
93
94    const std::string DSML         = "|DSML|";
95    const std::string THINK_START  = "<think>";
96    const std::string THINK_END    = "</think>";
97    const std::string TC_BLOCK     = is_v4 ? "tool_calls" : "function_calls";
98    const std::string FC_START     = "<" + DSML + TC_BLOCK + ">";
99    const std::string FC_END       = "</" + DSML + TC_BLOCK + ">";
100    const std::string INVOKE_START = "<" + DSML + "invoke";
101    const std::string INVOKE_END   = "</" + DSML + "invoke>";
102    const std::string PARAM_START  = "<" + DSML + "parameter";
103    const std::string PARAM_END    = "</" + DSML + "parameter>";
104    const std::string GEN_PROMPT   = "<|Assistant|>";
105    const std::string TC_SEPARATOR = "\n\n";
106
107    data.prompt = common_chat_template_direct_apply_impl(
108        tmpl, inputs, adjusted_messages, std::nullopt, additional_context);
109    data.generation_prompt = common_chat_template_generation_prompt_impl(
110        tmpl, inputs, adjusted_messages, std::nullopt, additional_context);
111    data.format             = COMMON_CHAT_FORMAT_PEG_NATIVE;
112    data.supports_thinking  = true;
113    data.thinking_start_tag = THINK_START;
114    data.thinking_end_tags  = {THINK_END, FC_START};
115    data.preserved_tokens   = {
116        DSML,
117        THINK_START,
118        THINK_END,
119    };
120
121    if (inputs.has_continuation()) {
122        const auto & msg = inputs.continue_msg;
123
124        if (is_v4 && msg.reasoning_content.empty()) {
125            data.generation_prompt = GEN_PROMPT + THINK_END;
126            if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
127                data.generation_prompt += msg.render_content();
128            }
129        } else {
130            data.generation_prompt = GEN_PROMPT + THINK_START + msg.reasoning_content;
131            if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
132                data.generation_prompt += THINK_END + msg.render_content();
133            }
134        }
135
136        data.prompt += data.generation_prompt;
137    }
138
139    bool require_tools   = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
140    bool has_tool_calls = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;
141
142    auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
143        auto generation_prompt = p.literal(GEN_PROMPT);
144        auto end               = p.end();
145
146        // build tool call section first since we might need it in reasoning
147        auto tool_choice = p.choice();
148        if (has_tool_calls) {
149            foreach_function(inputs.tools, [&](const json & tool) {
150                const auto & function = tool.at("function");
151                std::string  name     = function.at("name");
152
153                std::vector<common_peg_parser> required_parsers;
154                std::vector<common_peg_parser> optional_parsers;
155                foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) {
156                    bool is_string = param.schema->may_be_string();
157
158                    auto arg = p.tool_arg(
159                        p.tool_arg_open(p.literal(PARAM_START + " name=\"") + p.tool_arg_name(p.literal(param.name)) +
160                                        p.literal("\" string=\"" + std::string(is_string ? "true" : "false") + "\">")) +
161                        (is_string ?
162                             p.tool_arg_string_value(p.until(PARAM_END)) :
163                             p.tool_arg_json_value(p.schema(p.json(), "tool-" + name + "-arg-" + param.name + "-schema",
164                                                            doc, *param.schema))) +
165                        p.tool_arg_close(p.literal(PARAM_END)));
166
167                    auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg);
168                    if (param.required) {
169                        required_parsers.push_back(named_arg);
170                    } else {
171                        optional_parsers.push_back(named_arg);
172                    }
173                });
174
175                common_peg_parser args_seq = p.eps();
176                for (size_t i = 0; i < required_parsers.size(); i++) {
177                    if (i > 0) {
178                        args_seq = args_seq + p.space();
179                    }
180                    args_seq = args_seq + required_parsers[i];
181                }
182
183                if (!optional_parsers.empty()) {
184                    common_peg_parser any_opt = p.choice();
185                    for (const auto & opt : optional_parsers) {
186                        any_opt |= opt;
187                    }
188                    args_seq = args_seq + p.repeat(p.space() + any_opt, 0, -1);
189                }
190
191                common_peg_parser invoke_body = args_seq;
192                auto              func_parser = p.tool(p.tool_open(p.literal(INVOKE_START + " name=\"") +
193                                                                   p.tool_name(p.literal(name)) + p.literal("\">\n")) +
194                                                       invoke_body + p.space() + p.tool_close(p.literal(INVOKE_END)));
195
196                tool_choice |= p.rule("tool-" + name, func_parser);
197            });
198        }
199
200        common_peg_parser tool_calls = p.eps();
201        if (inputs.parallel_tool_calls) {
202            tool_calls = p.trigger_rule("tool-call",
203                p.literal(FC_START) + p.space() + tool_choice +
204                p.zero_or_more(p.space() + tool_choice) + p.space() + p.literal(FC_END));
205        } else {
206            tool_calls = p.trigger_rule("tool-call",
207                p.literal(FC_START) + p.space() + tool_choice + p.space() + p.literal(FC_END));
208        }
209
210        auto reasoning = p.eps();
211        auto reasoning_with_tc = p.eps();
212        auto obligatory_tool_calls = tool_calls;
213        bool allow_reasoning_with_tc = false;
214
215        if (!require_tools) {
216            tool_calls = p.optional(tool_calls);
217        }
218
219        if (extract_reasoning && inputs.enable_thinking) {
220            reasoning = p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END);
221            reasoning_with_tc = THINK_START +
222                p.reasoning(p.until_one_of({ TC_SEPARATOR + FC_START, FC_START, THINK_END })) +
223                p.space() + obligatory_tool_calls;
224            allow_reasoning_with_tc = true;
225        } else if (extract_reasoning) {
226            // Thinking disabled but reasoning extraction requested: the generation prompt
227            // contains an empty <think></think> pair (V3.2) or a bare </think> (V4) that
228            // must still be consumed.
229            reasoning = is_v4
230                ? p.optional(p.literal(THINK_END))
231                : p.optional(p.literal(THINK_START) + p.until(THINK_END) + p.literal(THINK_END));
232        }
233
234        if (has_response_format) {
235            auto response_format = p.rule("response-format",
236                p.literal("```json") + p.space() +
237                p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) +
238                p.space() + p.literal("```"));
239            return generation_prompt + reasoning + response_format + end;
240        }
241
242        if (!has_tool_calls) {
243            return generation_prompt + reasoning + p.content(p.rest()) + end;
244        }
245
246        auto content_before_tools = p.negate(p.literal(THINK_START)) +
247            p.content(p.until_one_of({ TC_SEPARATOR + FC_START, FC_START })) +
248            p.space();
249        return allow_reasoning_with_tc ? generation_prompt + (reasoning_with_tc | (reasoning + content_before_tools + tool_calls)) + end :
250            generation_prompt + reasoning + content_before_tools + tool_calls + end;
251    });
252
253    data.parser = parser.save();
254
255    if (include_grammar) {
256        data.grammar_lazy = has_tools && !require_tools;
257        data.grammar      = build_grammar([&](const common_grammar_builder & builder) {
258            parser.build_grammar(builder, data.grammar_lazy);
259        });
260
261        data.grammar_triggers = {
262            { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, FC_START },
263        };
264    }
265
266    return data;
267}