ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
chat_request.h
Go to the documentation of this file.
1#pragma once
2// A chat request as a prompt conversation (LOCAL_LLM_STACK.md section 3, points 3-5), shared by
3// the server's /chat and /v1/chat/completions (server.cpp), the WebAssembly ember_chat /
4// ember_prepare (deploy/wasm/ember_wasm.cpp) and tools/chat_template_check.cpp, so every path
5// renders through the same code:
6// request JSON -> Conversation (system, (user, assistant) turns, the new user message, thinking)
7// -> the context rule (build_chat_prompt) -> ChatFormat::prompt -> chat::render of the record.
8// Request errors throw RequestError, the template's refusals chat::Error; both are HTTP 400.
9// Messages as keramos IR_SPEC 8.1.1 reads them (v1.4.2): every history entry is an object with a string
10// content (an assistant entry with tool_calls may leave it null: ""), or a pair of strings; tool_calls (even
11// null or []) and tool_call_id are refused under a record without tool turns, and under one with them on any
12// message but an assistant's and a tool's respectively; a null tool_call_id answers a call without an id.
13//
14// Records with tool turns (paperkiln/2, chat::Template::has_tool; schema v2): the request becomes the
15// whole message list (Conversation::general), tool calls and results included, plus the request's
16// tool definitions, and the context rule drops the oldest whole exchange (chat::exchanges). Behaviour
17// follows the resolved record's fields: under every other record (paperkiln/1 and the rest) the v1
18// rules below are unchanged, and tool definitions, tool messages and tool_calls are HTTP 400.
19#include <stdexcept>
20#include <string>
21#include <utility>
22#include <vector>
23
24#include <nlohmann/json.hpp>
25
26#include "chat_tokenizer.h"
27
28namespace ember {
29
30struct RequestError : std::runtime_error {
31 using std::runtime_error::runtime_error;
32};
33
35 std::string system; // used when has_system
36 bool has_system = false;
37 std::vector<std::pair<std::string, std::string>> turns; // (user, assistant), oldest first
38 std::string user;
39 int thinking = -1; // -1 the template's default, 0 off, 1 on
40 // A record with tool turns: `messages` is the whole prompt conversation (a non-empty system
41 // message first, then user, assistant and tool messages; the v1 fields above unused but
42 // thinking), `tools` the tool definitions (member order kept: chat::json is ordered_json).
43 bool general = false;
44 std::vector<chat::Message> messages;
45 std::vector<chat::json> tools;
46};
47
48namespace detail {
49inline int thinking_field(const nlohmann::json& v, const char* name) {
50 if (!v.is_boolean()) throw RequestError(std::string(name) + " must be true or false");
51 return v.get<bool>() ? 1 : 0;
52}
53
54inline bool tool_turns(const chat::Template* t) { return t && t->has_tool; }
55
56// A request's "tools" under a record without tool turns: absent, null or [] (no definitions);
57// anything else is refused.
58inline void refuse_tools_v1(const nlohmann::json& r) {
59 if (!r.is_object() || !r.contains("tools") || r["tools"].is_null()) return;
60 if (r["tools"].is_array() && r["tools"].empty()) return;
61 throw RequestError("tools are not supported: this model's chat template has no tool turns");
62}
63
64// The tool definitions: `ordered` (the request's "tools" as chat::json, member order kept; null when
65// the caller has none) or, failing that, r["tools"] (member order then alphabetical). A list of
66// objects; absent, null or [] is none.
67inline std::vector<chat::json> tool_definitions(const nlohmann::json& r, const chat::json& ordered) {
68 const chat::json src = !ordered.is_null() ? ordered
69 : r.is_object() && r.contains("tools") ? chat::json::parse(r["tools"].dump())
70 : chat::json();
71 std::vector<chat::json> out;
72 if (src.is_null()) return out;
73 if (!src.is_array()) throw RequestError("tools must be a list of tool definitions (JSON objects)");
74 for (const auto& d : src) {
75 if (!d.is_object()) throw RequestError("tools must be a list of tool definitions (JSON objects)");
76 out.push_back(d);
77 }
78 return out;
79}
80
81// A history entry's content (IR_SPEC 8.1.1: every message has a string content). Missing or null is
82// refused, except on an assistant entry with tool_calls (OpenAI's null content beside calls), where it is "".
83inline std::string history_content(const nlohmann::json& h, size_t i, bool calls_may_omit = false) {
84 const std::string at = "history[" + std::to_string(i) + "]";
85 const bool calls = h.contains("tool_calls") && !h["tool_calls"].is_null();
86 if (!h.contains("content") || h["content"].is_null()) {
87 if (calls_may_omit && calls && h.value("role", "") == "assistant") return std::string();
88 throw RequestError(at + " needs a string content (an assistant entry with tool_calls may leave it null)");
89 }
90 if (!h["content"].is_string()) throw RequestError(at + ".content must be a string");
91 return h["content"].get<std::string>();
92}
93
94// A message's tool_calls or tool_call_id under a record without tool turns (IR_SPEC 8.1.1): any such member,
95// null or an empty list included, is refused.
96inline void refuse_tool_members_v1(const nlohmann::json& m, const std::string& at) {
97 if (m.contains("tool_calls"))
98 throw RequestError(at + ": tool_calls are not supported: this model's chat template has no tool turns");
99 if (m.contains("tool_call_id"))
100 throw RequestError(at + ": tool_call_id is not supported: this model's chat template has no tool messages");
101}
102
103// A {user, assistant} pair entry: both strings.
104inline std::pair<std::string, std::string> history_pair(const nlohmann::json& h, size_t i) {
105 const std::string at = "history[" + std::to_string(i) + "]";
106 if (!h.contains("user") || !h["user"].is_string() || !h.contains("assistant") || !h["assistant"].is_string())
107 throw RequestError(at + " needs a string role and content, or string user and assistant");
108 return {h["user"].get<std::string>(), h["assistant"].get<std::string>()};
109}
110
111// One message of a tool-turn conversation, through the header's parser (its tool_calls and
112// tool_call_id rules): role, content, tool_calls and tool_call_id as given; render's structure rules
113// decide whether they belong where they are.
114inline chat::Message tool_turn_message(const std::string& role, const std::string& content, const nlohmann::json& m) {
115 chat::json x = chat::json::object();
116 x["role"] = role;
117 x["content"] = content;
118 if (m.contains("tool_calls")) x["tool_calls"] = chat::json::parse(m["tool_calls"].dump());
119 if (m.contains("tool_call_id")) x["tool_call_id"] = chat::json::parse(m["tool_call_id"].dump());
120 try {
121 return chat::messages_from_json(chat::json::array({x}))[0];
122 } catch (const chat::Error& e) {
123 throw RequestError(e.what());
124 }
125}
126
127// The leading system message of a tool-turn conversation, when it is non-empty (an empty one is none).
128inline void push_system(std::vector<chat::Message>& m, const std::string& s) {
129 if (!s.empty()) m.insert(m.begin(), chat::Message("system", s));
130}
131} // namespace detail
132
133// The request's "tools" with the member order the client wrote: the body parsed as chat::json
134// (ordered_json). Null when the body has none or is not JSON.
135inline chat::json ordered_tools(const std::string& body) {
136 chat::json o = chat::json::parse(body, nullptr, false);
137 if (o.is_discarded() || !o.is_object() || !o.contains("tools")) return chat::json();
138 return o["tools"];
139}
140
141// POST /chat under a record with tool turns: "system" or a leading {"role": "system"} history entry
142// (not both, nowhere else; empty means none); history entries {"role", "content", "tool_calls"?,
143// "tool_call_id"?} (roles user, assistant, tool) or {"user", "assistant"} pairs; user_input, which may
144// be omitted when the history ends on a tool message; "tools"; "thinking". The order of user,
145// assistant and tool messages is the record's structure rule (chat::check_structure, at render).
146inline Conversation chat_conversation_tools(const nlohmann::json& r, const chat::json& tools) {
148 c.general = true;
149 std::string system;
150 bool has_system = false;
151 if (r.contains("system")) {
152 if (!r["system"].is_string()) throw RequestError("system must be a string");
153 system = r["system"].get<std::string>();
154 has_system = true;
155 }
156 if (r.contains("history") && r["history"].is_array()) {
157 const auto& hist = r["history"];
158 for (size_t i = 0; i < hist.size(); ++i) {
159 const auto& h = hist[i];
160 if (!h.is_object()) throw RequestError("history[" + std::to_string(i) + "] must be an object");
161 if (h.contains("role")) {
162 if (!h["role"].is_string()) throw RequestError("history[" + std::to_string(i) + "].role must be a string");
163 const std::string role = h["role"].get<std::string>();
164 const std::string content = detail::history_content(h, i, /*calls_may_omit=*/true);
165 if (role == "system") {
166 if (h.contains("tool_calls") || h.contains("tool_call_id"))
167 throw RequestError("history[" + std::to_string(i) +
168 "]: tool_calls belong on assistant messages, tool_call_id on tool messages");
169 if (i != 0) throw RequestError("a system message is allowed only as the first history entry");
170 if (has_system)
171 throw RequestError("give the system message once: the 'system' field or a leading history entry");
172 system = content;
173 has_system = true;
174 } else if (role == "user" || role == "assistant" || role == "tool") {
175 c.messages.push_back(detail::tool_turn_message(role, content, h));
176 } else {
177 throw RequestError("history role '" + role +
178 "' is not allowed here (user, assistant, tool; system only first)");
179 }
180 } else {
181 const auto pr = detail::history_pair(h, i);
182 c.messages.push_back({"user", pr.first});
183 c.messages.push_back({"assistant", pr.second});
184 }
185 }
186 }
187 if (r.contains("user_input")) {
188 if (!r["user_input"].is_string()) throw RequestError("user_input must be a string");
189 c.messages.push_back({"user", r["user_input"].get<std::string>()});
190 } else if (c.messages.empty() || c.messages.back().role != "tool") {
191 throw RequestError("Missing 'user_input' field in request JSON (it may be left out only when the history "
192 "ends on a tool message)");
193 }
194 detail::push_system(c.messages, system);
195 c.tools = detail::tool_definitions(r, tools);
196 if (r.contains("thinking")) c.thinking = detail::thinking_field(r["thinking"], "thinking");
197 return c;
198}
199
200// POST /chat (point 4): user_input; history as [{"role", "content"}] or [{"user", "assistant"}]. A
201// role-form history pairs strictly, as mtstudio's history_turns (paperkiln 7a6c7659; section 3 point
202// 3a): after an optional leading system entry, user and assistant alternate, starting with user and
203// ending with assistant; anything else is a 400 naming the rule, in mtstudio's words. Pair-form
204// entries are whole turns. The system message as "system" or
205// a leading {"role": "system"} history entry, not both and nowhere else (empty means none); and
206// "thinking" (a template's no-think generation prompt when false).
207// t: the model's record; one with tool turns takes chat_conversation_tools (tools: the request's
208// "tools" from ordered_tools(body), or null). Under any other record a non-empty "tools" or a
209// history entry's non-empty "tool_calls" is refused, as is the tool role.
210inline Conversation chat_conversation(const nlohmann::json& r, const chat::Template* t = nullptr,
211 const chat::json& tools = chat::json()) {
212 if (detail::tool_turns(t)) return chat_conversation_tools(r, tools);
214 if (!r.contains("user_input") || !r["user_input"].is_string())
215 throw RequestError("Missing 'user_input' field in request JSON");
216 c.user = r["user_input"].get<std::string>();
217 if (r.contains("system")) {
218 if (!r["system"].is_string()) throw RequestError("system must be a string");
219 c.system = r["system"].get<std::string>();
220 c.has_system = true;
221 }
222 if (r.contains("history") && r["history"].is_array()) {
223 const auto& hist = r["history"];
224 std::string pending_user;
225 bool have_user = false;
226 for (size_t i = 0; i < hist.size(); ++i) {
227 const auto& h = hist[i];
228 if (!h.is_object()) throw RequestError("history[" + std::to_string(i) + "] must be an object");
229 if (h.contains("role")) {
230 if (!h["role"].is_string()) throw RequestError("history[" + std::to_string(i) + "].role must be a string");
231 const std::string role = h["role"].get<std::string>();
232 detail::refuse_tool_members_v1(h, "history[" + std::to_string(i) + "]");
233 const std::string content = detail::history_content(h, i);
234 if (role == "system") {
235 if (i != 0) throw RequestError("a system message is allowed only as the first history entry");
236 if (c.has_system)
237 throw RequestError("give the system message once: the 'system' field or a leading history entry");
238 c.system = content;
239 c.has_system = true;
240 } else if (role == "user") {
241 if (have_user)
242 throw RequestError("history has two user messages in a row: user and assistant messages "
243 "must alternate (section 3 point 3a)");
244 pending_user = content;
245 have_user = true;
246 } else if (role == "assistant") {
247 if (!have_user)
248 throw RequestError("history has an assistant message without a user message before it: user "
249 "and assistant messages must alternate, starting with user (section 3 "
250 "point 3a)");
251 c.turns.emplace_back(pending_user, content);
252 have_user = false;
253 } else {
254 throw RequestError("history role '" + role +
255 "' is not allowed here (user, assistant; system only first)");
256 }
257 } else {
258 if (have_user)
259 throw RequestError("history has a user message without its assistant reply before a {user, "
260 "assistant} pair");
261 c.turns.push_back(detail::history_pair(h, i));
262 }
263 }
264 if (have_user)
265 throw RequestError("history ends with a user message: it must end with an assistant reply (the new "
266 "message goes in user_input)");
267 }
268 c.has_system = c.has_system && !c.system.empty();
269 if (r.contains("thinking")) c.thinking = detail::thinking_field(r["thinking"], "thinking");
271 return c;
272}
273
274// The text of an OpenAI message's content: a string, or text parts joined; anything else refused.
275inline std::string openai_content(const nlohmann::json& m, size_t i) {
276 const std::string at = "messages[" + std::to_string(i) + "]";
277 if (!m.contains("content") || m["content"].is_null()) throw RequestError(at + " has no content");
278 const auto& c = m["content"];
279 if (c.is_string()) return c.get<std::string>();
280 if (!c.is_array()) throw RequestError(at + ".content must be a string or a list of text parts");
281 std::string s;
282 for (const auto& part : c) {
283 if (!part.is_object() || part.value("type", "") != "text" || !part.contains("text") || !part["text"].is_string())
284 throw RequestError(at + ".content: only text parts are supported (this model takes text only)");
285 s += part["text"].get<std::string>();
286 }
287 return s;
288}
289
290namespace detail {
291inline void openai_thinking(const nlohmann::json& body, Conversation& c) {
292 if (body.contains("thinking")) c.thinking = thinking_field(body["thinking"], "thinking");
293 if (body.contains("chat_template_kwargs") && body["chat_template_kwargs"].is_object() &&
294 body["chat_template_kwargs"].contains("enable_thinking"))
295 c.thinking = thinking_field(body["chat_template_kwargs"]["enable_thinking"],
296 "chat_template_kwargs.enable_thinking");
297}
298} // namespace detail
299
300// POST /v1/chat/completions under a record with tool turns: system and developer messages before the
301// first other message join as under v1 (in order, "\n\n", an empty one dropped; a later one is
302// refused); user, assistant and tool messages map one to one, an assistant message's tool_calls (its
303// content may then be null) and a tool message's tool_call_id included; "tools" passes through; the
304// last message is the user's or a tool's. tool_choice: absent, null or "auto" (the model decides).
305inline Conversation openai_conversation_tools(const nlohmann::json& body, const chat::json& tools) {
306 const auto& msgs = body["messages"];
308 c.general = true;
309 std::string system;
310 for (size_t i = 0; i < msgs.size(); ++i) {
311 const auto& x = msgs[i];
312 if (!x.is_object() || !x.contains("role") || !x["role"].is_string())
313 throw RequestError("messages[" + std::to_string(i) + "] needs a string role");
314 const std::string role = x["role"].get<std::string>();
315 if (role == "system" || role == "developer") {
316 if (!c.messages.empty())
317 throw RequestError("a " + role +
318 " message is allowed only before the first user message (this model's chat "
319 "template has one system message, first)");
320 if (x.contains("tool_calls") || x.contains("tool_call_id"))
321 throw RequestError("messages[" + std::to_string(i) +
322 "]: tool_calls belong on assistant messages, tool_call_id on tool messages");
323 const std::string s = openai_content(x, i);
324 if (s.empty()) continue;
325 system += (system.empty() ? "" : "\n\n") + s;
326 continue;
327 }
328 if (role != "user" && role != "assistant" && role != "tool")
329 throw RequestError("messages[" + std::to_string(i) + "] has unknown role '" + role + "'");
330 const bool calls = x.contains("tool_calls") && !x["tool_calls"].is_null();
331 const bool no_content = !x.contains("content") || x["content"].is_null();
332 const std::string content = role == "assistant" && calls && no_content ? std::string() : openai_content(x, i);
333 c.messages.push_back(detail::tool_turn_message(role, content, x));
334 }
335 if (c.messages.empty() || (c.messages.back().role != "user" && c.messages.back().role != "tool"))
336 throw RequestError("the last message must be from the user or a tool");
337 detail::push_system(c.messages, system);
338 c.tools = detail::tool_definitions(body, tools);
339 if (body.contains("tool_choice") && !body["tool_choice"].is_null() &&
340 !(body["tool_choice"].is_string() && body["tool_choice"].get<std::string>() == "auto"))
341 throw RequestError("tool_choice is not supported except \"auto\" (the model decides whether to call a tool)");
343 return c;
344}
345
346// POST /v1/chat/completions (point 4): messages map one to one. System and developer messages
347// before the first user message join, in order, with "\n\n" into the system message (an empty
348// one is dropped); a later one, or a tool message, is refused; user and assistant messages must
349// alternate from the user, and the last message must be from the user. Thinking: "thinking" or
350// chat_template_kwargs.enable_thinking (llama.cpp's and vLLM's field), both booleans.
351// t and tools as chat_conversation's: a record with tool turns takes openai_conversation_tools;
352// under any other a non-empty "tools" or an assistant message's non-empty tool_calls is refused.
353inline Conversation openai_conversation(const nlohmann::json& body, const chat::Template* t = nullptr,
354 const chat::json& tools = chat::json()) {
355 if (!body.is_object() || !body.contains("messages") || !body["messages"].is_array() || body["messages"].empty())
356 throw RequestError("body must be JSON with a non-empty 'messages' array");
358 const auto& msgs = body["messages"];
360 std::vector<chat::Message> m;
361 for (size_t i = 0; i < msgs.size(); ++i) {
362 const auto& x = msgs[i];
363 if (!x.is_object() || !x.contains("role") || !x["role"].is_string())
364 throw RequestError("messages[" + std::to_string(i) + "] needs a string role");
365 const std::string role = x["role"].get<std::string>();
366 if (role == "tool")
367 throw RequestError("tool messages are not supported: this model's chat template has no tool role");
368 detail::refuse_tool_members_v1(x, "messages[" + std::to_string(i) + "]");
369 if (role == "system" || role == "developer") {
370 if (!m.empty())
371 throw RequestError("a " + role +
372 " message is allowed only before the first user message (this model's chat "
373 "template has one system message, first)");
374 const std::string s = openai_content(x, i);
375 if (s.empty()) continue;
376 c.system += (c.has_system ? "\n\n" : "") + s;
377 c.has_system = true;
378 continue;
379 }
380 if (role != "user" && role != "assistant")
381 throw RequestError("messages[" + std::to_string(i) + "] has unknown role '" + role + "'");
382 m.push_back({role, openai_content(x, i)});
383 }
384 if (m.empty() || m.back().role != "user") throw RequestError("the last message must be from the user");
385 for (size_t i = 0; i < m.size(); ++i)
386 if (m[i].role != (i % 2 == 0 ? "user" : "assistant"))
387 throw RequestError("messages must alternate user and assistant, starting from the user (message " +
388 std::to_string(i) + " is " + m[i].role + ")");
389 for (size_t i = 0; i + 1 < m.size(); i += 2) c.turns.emplace_back(m[i].content, m[i + 1].content);
390 c.user = m.back().content;
393 return c;
394}
395
397 std::string prompt;
399 int turns_used = 0;
400 std::string error; // non-empty: the newest message (with the system message) alone does not fit
401};
402
403// The context rule for a tool-turn conversation (Conversation::general): an exchange is a user
404// message and everything before the next user message (replies, tool calls, tool results;
405// chat::exchanges). While more than the newest exchange remains and prompt tokens + max_new > ctx,
406// the OLDEST whole exchange is dropped; the system turn, with its tool definitions, is pinned. When
407// the system turn and the newest exchange alone are >= ctx tokens, the request is refused naming the
408// counts; an exchange is never cut. history_turns_used counts the past exchanges kept (as v1's past
409// turns: for a user/assistant-only conversation the same number).
410template <class Count>
412 const std::vector<chat::Message>& m = c.messages;
413 const size_t sys = !m.empty() && m[0].role == "system" ? 1 : 0;
414 const auto ex = chat::exchanges(m);
415 auto kept = [&](size_t k) {
416 if (k == 0) return m; // the whole conversation: anything before the first user message is refused by render
417 std::vector<chat::Message> out(m.begin(), m.begin() + static_cast<std::ptrdiff_t>(sys));
418 if (k < ex.size()) out.insert(out.end(), m.begin() + static_cast<std::ptrdiff_t>(ex[k].first), m.end());
419 return out;
420 };
421 auto build = [&](size_t k) { return chat::render(kept(k), f.tmpl, /*generation_prompt=*/true, c.thinking, c.tools); };
422 size_t k = 0;
424 out.prompt = build(k); // the whole conversation first: the record's refusals come from here
425 while (k + 1 < ex.size() && count(out.prompt) + max_new > ctx) out.prompt = build(++k);
426 out.prompt_tokens = count(out.prompt);
427 out.turns_used = ex.empty() ? 0 : static_cast<int>(ex.size() - 1 - k);
428 if (out.prompt_tokens >= ctx) {
429 std::string head; // the pinned system turn (with the tool definitions), as rendered
430 for (const auto& p : chat::render_pieces(kept(k), f.tmpl, true, c.thinking, c.tools))
431 if (p.role == "system" || p.kind == "bos") head += p.text;
432 const int pinned = head.empty() ? 0 : count(head);
433 out.error = pinned > 0 ? "the system turn and the newest exchange are " + std::to_string(out.prompt_tokens) +
434 " tokens (the system turn " + std::to_string(pinned) +
435 "); the model's context is " + std::to_string(ctx) +
436 " (an exchange is never cut)"
437 : "the newest exchange is " + std::to_string(out.prompt_tokens) +
438 " tokens; the model's context is " + std::to_string(ctx) +
439 " (an exchange is never cut)";
440 }
441 return out;
442}
443
444// The context rule (point 5): the prompt from the past turns oldest first plus the new message;
445// while turns remain and prompt tokens + max_new > ctx, the OLDEST whole exchange is dropped. The
446// system message is pinned (fold mode: it folds into the first exchange kept) and never cut.
447// count(text) is the token count generate() will run (state ids included).
448template <class Count>
450 if (c.general) return build_general_prompt(f, c, max_new, ctx, count);
451 const std::string* sys = c.has_system ? &c.system : nullptr;
452 auto build = [&](size_t from) { return f.prompt(c.turns, from, c.user, sys, c.thinking); };
453 size_t from = 0;
455 out.prompt = build(from);
456 while (from < c.turns.size() && count(out.prompt) + max_new > ctx) out.prompt = build(++from);
457 out.prompt_tokens = count(out.prompt);
458 out.turns_used = static_cast<int>(c.turns.size() - from);
459 if (out.prompt_tokens >= ctx)
460 out.error = "message is " + std::to_string(out.prompt_tokens) + " tokens; the model's context is " +
461 std::to_string(ctx);
462 return out;
463}
464
465} // namespace ember
const float * p[kIrDepthSourcesMax]
std::vector< float > v
const char * name
float c
node out
size_t j
bool tool_turns(const chat::Template *t)
chat::Message tool_turn_message(const std::string &role, const std::string &content, const nlohmann::json &m)
std::pair< std::string, std::string > history_pair(const nlohmann::json &h, size_t i)
std::string history_content(const nlohmann::json &h, size_t i, bool calls_may_omit=false)
void refuse_tools_v1(const nlohmann::json &r)
void refuse_tool_members_v1(const nlohmann::json &m, const std::string &at)
int thinking_field(const nlohmann::json &v, const char *name)
std::vector< chat::json > tool_definitions(const nlohmann::json &r, const chat::json &ordered)
void openai_thinking(const nlohmann::json &body, Conversation &c)
void push_system(std::vector< chat::Message > &m, const std::string &s)
Conversation openai_conversation(const nlohmann::json &body, const chat::Template *t=nullptr, const chat::json &tools=chat::json())
Conversation chat_conversation_tools(const nlohmann::json &r, const chat::json &tools)
ChatPrompt build_general_prompt(const ChatFormat &f, const Conversation &c, int max_new, int ctx, Count count)
chat::json ordered_tools(const std::string &body)
std::string openai_content(const nlohmann::json &m, size_t i)
Conversation chat_conversation(const nlohmann::json &r, const chat::Template *t=nullptr, const chat::json &tools=chat::json())
Conversation openai_conversation_tools(const nlohmann::json &body, const chat::json &tools)
ChatPrompt build_chat_prompt(const ChatFormat &f, const Conversation &c, int max_new, int ctx, Count count)
std::vector< Message > messages_from_json(const J &a)
std::vector< Piece > render_pieces(const std::vector< Message > &m, const Template &t, bool generation_prompt, int thinking=-1, const std::vector< json > &tools={})
std::vector< std::pair< size_t, size_t > > exchanges(const std::vector< Message > &m)
std::string render(const std::vector< Message > &m, const Template &t, bool generation_prompt, int thinking=-1, const std::vector< json > &tools={})
nlohmann::ordered_json json
std::string error
std::string prompt
std::vector< std::pair< std::string, std::string > > turns
std::vector< chat::Message > messages
std::vector< chat::json > tools