24#include <nlohmann/json.hpp>
31 using std::runtime_error::runtime_error;
37 std::vector<std::pair<std::string, std::string>>
turns;
50 if (!
v.is_boolean())
throw RequestError(std::string(
name) +
" must be true or false");
51 return v.get<
bool>() ? 1 : 0;
59 if (!
r.is_object() || !
r.contains(
"tools") ||
r[
"tools"].is_null())
return;
61 throw RequestError(
"tools are not supported: this model's chat template has no tool turns");
69 :
r.is_object() &&
r.contains(
"tools") ? chat::json::parse(
r[
"tools"].
dump())
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)");
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()) {
88 throw RequestError(
at +
" needs a string content (an assistant entry with tool_calls may leave it null)");
91 return h[
"content"].get<std::string>();
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");
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");
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());
128inline void push_system(std::vector<chat::Message>&
m,
const std::string&
s) {
137 if (
o.is_discarded() || !
o.is_object() || !
o.contains(
"tools"))
return chat::json();
150 bool has_system =
false;
151 if (
r.contains(
"system")) {
153 system =
r[
"system"].get<std::string>();
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) {
160 if (!
h.is_object())
throw RequestError(
"history[" + std::to_string(
i) +
"] must be an object");
161 if (
h.contains(
"role")) {
163 const std::string role =
h[
"role"].get<std::string>();
165 if (role ==
"system") {
166 if (
h.contains(
"tool_calls") ||
h.contains(
"tool_call_id"))
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");
171 throw RequestError(
"give the system message once: the 'system' field or a leading history entry");
174 }
else if (role ==
"user" || role ==
"assistant" || role ==
"tool") {
178 "' is not allowed here (user, assistant, tool; system only first)");
182 c.messages.push_back({
"user",
pr.first});
183 c.messages.push_back({
"assistant",
pr.second});
187 if (
r.contains(
"user_input")) {
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)");
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")) {
219 c.system =
r[
"system"].get<std::string>();
222 if (
r.contains(
"history") &&
r[
"history"].is_array()) {
223 const auto&
hist =
r[
"history"];
226 for (
size_t i = 0;
i <
hist.size(); ++
i) {
228 if (!
h.is_object())
throw RequestError(
"history[" + std::to_string(
i) +
"] must be an object");
229 if (
h.contains(
"role")) {
231 const std::string role =
h[
"role"].get<std::string>();
234 if (role ==
"system") {
235 if (
i != 0)
throw RequestError(
"a system message is allowed only as the first history entry");
237 throw RequestError(
"give the system message once: the 'system' field or a leading history entry");
240 }
else if (role ==
"user") {
242 throw RequestError(
"history has two user messages in a row: user and assistant messages "
243 "must alternate (section 3 point 3a)");
246 }
else if (role ==
"assistant") {
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 "
255 "' is not allowed here (user, assistant; system only first)");
259 throw RequestError(
"history has a user message without its assistant reply before a {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)");
268 c.has_system =
c.has_system && !
c.system.empty();
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");
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>();
293 if (
body.contains(
"chat_template_kwargs") &&
body[
"chat_template_kwargs"].is_object() &&
294 body[
"chat_template_kwargs"].contains(
"enable_thinking"))
296 "chat_template_kwargs.enable_thinking");
306 const auto&
msgs =
body[
"messages"];
310 for (
size_t i = 0;
i <
msgs.size(); ++
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())
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"))
322 "]: tool_calls belong on assistant messages, tool_call_id on tool messages");
324 if (
s.empty())
continue;
325 system += (system.empty() ?
"" :
"\n\n") +
s;
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();
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");
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)");
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) {
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>();
367 throw RequestError(
"tool messages are not supported: this model's chat template has no tool role");
369 if (role ==
"system" || role ==
"developer") {
372 " message is allowed only before the first user message (this model's chat "
373 "template has one system message, first)");
375 if (
s.empty())
continue;
376 c.system += (
c.has_system ?
"\n\n" :
"") +
s;
380 if (role !=
"user" && role !=
"assistant")
381 throw RequestError(
"messages[" + std::to_string(
i) +
"] has unknown role '" + role +
"'");
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;
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;
415 auto kept = [&](
size_t k) {
416 if (
k == 0)
return m;
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());
427 out.turns_used =
ex.empty() ? 0 :
static_cast<int>(
ex.size() - 1 -
k);
428 if (
out.prompt_tokens >= ctx) {
431 if (
p.role ==
"system" ||
p.kind ==
"bos")
head +=
p.text;
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)";
448template <
class 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); };
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 " +
const float * p[kIrDepthSourcesMax]
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::vector< std::pair< std::string, std::string > > turns
std::vector< chat::Message > messages
std::vector< chat::json > tools