|
| 1 | +#include "parsers.h" |
| 2 | + |
| 3 | +// Inkling / TML typed-content-block parser: <|end_message|> separates blocks within a turn, |
| 4 | +// <|content_model_end_sampling|> is the sole end-of-generation token (mirrors sglang TmlDetector). |
| 5 | +common_chat_params common_chat_params_init_inkling(const common_chat_template & tmpl, |
| 6 | + const autoparser::generation_params & inputs) { |
| 7 | + common_chat_params data; |
| 8 | + |
| 9 | + const std::string MSG_MODEL = "<|message_model|>"; |
| 10 | + const std::string MSG_USER = "<|message_user|>"; |
| 11 | + const std::string MSG_SYSTEM = "<|message_system|>"; |
| 12 | + const std::string MSG_TOOL = "<|message_tool|>"; |
| 13 | + const std::string THINK = "<|content_thinking|>"; |
| 14 | + const std::string TEXT = "<|content_text|>"; |
| 15 | + const std::string END_MESSAGE = "<|end_message|>"; |
| 16 | + const std::string END_SAMPLING = "<|content_model_end_sampling|>"; |
| 17 | + const std::string INVOKE_TOOL = "<|content_invoke_tool_json|>"; |
| 18 | + |
| 19 | + data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs); |
| 20 | + data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs); |
| 21 | + data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; |
| 22 | + data.supports_thinking = true; |
| 23 | + data.thinking_start_tag = THINK; |
| 24 | + data.thinking_end_tags = {END_MESSAGE}; |
| 25 | + data.preserved_tokens = { |
| 26 | + MSG_MODEL, MSG_USER, MSG_SYSTEM, MSG_TOOL, |
| 27 | + THINK, TEXT, END_MESSAGE, END_SAMPLING, INVOKE_TOOL, |
| 28 | + }; |
| 29 | + |
| 30 | + auto has_tools = inputs.tools.is_array() && !inputs.tools.empty(); |
| 31 | + data.message_delimiters = { |
| 32 | + { COMMON_CHAT_ROLE_ASSISTANT, MSG_MODEL }, |
| 33 | + { COMMON_CHAT_ROLE_USER, MSG_USER }, |
| 34 | + { COMMON_CHAT_ROLE_SYSTEM, MSG_SYSTEM }, |
| 35 | + { COMMON_CHAT_ROLE_TOOL, MSG_TOOL }, |
| 36 | + }; |
| 37 | + |
| 38 | + auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; |
| 39 | + |
| 40 | + if (inputs.has_continuation()) { |
| 41 | + const auto & msg = inputs.continue_msg; |
| 42 | + |
| 43 | + data.generation_prompt = MSG_MODEL + THINK + msg.reasoning_content; |
| 44 | + if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) { |
| 45 | + data.generation_prompt += END_MESSAGE + TEXT + msg.render_content(); |
| 46 | + } |
| 47 | + |
| 48 | + data.prompt += data.generation_prompt; |
| 49 | + } |
| 50 | + |
| 51 | + auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { |
| 52 | + auto generation_prompt = p.literal(MSG_MODEL); |
| 53 | + auto end = p.end(); |
| 54 | + |
| 55 | + // thinking block; may also reappear mid-turn (after content), so it is both an optional |
| 56 | + // prefix and a choice inside the block loops. With reasoning_format=NONE keep it |
| 57 | + // (markers included) inline as content |
| 58 | + common_peg_parser reasoning_block = p.eps(); |
| 59 | + if (extract_reasoning) { |
| 60 | + reasoning_block = p.literal(THINK) + |
| 61 | + p.reasoning(p.until_one_of({ END_MESSAGE, TEXT, END_SAMPLING })) + |
| 62 | + p.optional(p.literal(END_MESSAGE)); |
| 63 | + } else { |
| 64 | + reasoning_block = p.content(p.literal(THINK) + |
| 65 | + p.until_one_of({ END_MESSAGE, TEXT, END_SAMPLING }) + |
| 66 | + p.optional(p.literal(END_MESSAGE))); |
| 67 | + } |
| 68 | + auto reasoning = p.optional(reasoning_block); |
| 69 | + |
| 70 | + // TML re-emits <|message_model|> before each content block; a turn may contain several |
| 71 | + // text blocks (one per content part), so the block repeats and bodies concatenate. |
| 72 | + // THINK stops the content scan so a mid-turn thinking block is never leaked as text |
| 73 | + auto text_block = p.optional(p.literal(MSG_MODEL)) + |
| 74 | + p.optional(p.literal(TEXT)) + |
| 75 | + p.content(p.until_one_of({ THINK, END_MESSAGE, END_SAMPLING })) + |
| 76 | + p.optional(p.literal(END_MESSAGE)); |
| 77 | + auto text_content = p.one_or_more(p.choice({ reasoning_block, text_block })); |
| 78 | + |
| 79 | + if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) { |
| 80 | + return generation_prompt + reasoning + text_content + |
| 81 | + p.optional(p.literal(END_SAMPLING)) + end; |
| 82 | + } |
| 83 | + |
| 84 | + // each call is its own block (role opener + bare name echo + JSON section); |
| 85 | + // force_tool_calls=true makes the JSON section required so a pure-text answer fails the |
| 86 | + // block cleanly; parallel calls are separate blocks, hence repeat + parallel=false |
| 87 | + auto tool_section = p.standard_json_tools( |
| 88 | + INVOKE_TOOL, END_MESSAGE, inputs.tools, /* parallel_tool_calls = */ false, |
| 89 | + /* force_tool_calls = */ true, |
| 90 | + /* name_key = */ "name", |
| 91 | + /* args_key = */ "args", |
| 92 | + /* array_wrapped = */ false, |
| 93 | + /* function_is_key = */ false, |
| 94 | + /* call_id_key = */ "", |
| 95 | + /* gen_call_id_key = */ "", |
| 96 | + /* parameters_order = */ {}, |
| 97 | + /* accept_openai_wrapper = */ false, |
| 98 | + /* require_object_args = */ true); |
| 99 | + // the name-echo scan must stop at any block marker: a greedy until(INVOKE_TOOL) returns |
| 100 | + // NEED_MORE_INPUT mid-stream, which choice() treats as a match and shadows the text branch |
| 101 | + auto tool_block = p.optional(p.literal(MSG_MODEL)) + |
| 102 | + p.until_one_of({ INVOKE_TOOL, TEXT, THINK, END_MESSAGE, END_SAMPLING }) + |
| 103 | + tool_section; |
| 104 | + auto tool_calls = inputs.parallel_tool_calls ? p.one_or_more(tool_block) : tool_block; |
| 105 | + // turns may interleave narration, thinking and calls; parse block-by-block (tool block |
| 106 | + // first) since a whole-body choice would let the text branch swallow tool blocks into |
| 107 | + // visible content |
| 108 | + auto mixed_body = p.one_or_more(p.choice({ tool_block, reasoning_block, text_block })); |
| 109 | + auto body = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED |
| 110 | + ? tool_calls |
| 111 | + : mixed_body; |
| 112 | + |
| 113 | + return generation_prompt + reasoning + body + |
| 114 | + p.optional(p.literal(END_SAMPLING)) + end; |
| 115 | + }); |
| 116 | + |
| 117 | + data.parser = parser.save(); |
| 118 | + |
| 119 | + return data; |
| 120 | +} |
0 commit comments