#include "conv_template.h" #include #include "../support/json_parser.h" #include "image_utils.h" namespace mlc { namespace llm { namespace json_ffi { using namespace mlc::llm; /****************** Model vision config ******************/ ModelVisionConfig ModelVisionConfig::FromJSON(const tvm::ffi::json::Object& json_obj) { ModelVisionConfig config; Result hidden_size_res = json::LookupWithResultReturn(json_obj, "hidden_size"); if (hidden_size_res.IsOk()) { config.hidden_size = static_cast(hidden_size_res.Unwrap()); } Result image_size_res = json::LookupWithResultReturn(json_obj, "image_size"); if (image_size_res.IsOk()) { config.image_size = static_cast(image_size_res.Unwrap()); } Result intermediate_size_res = json::LookupWithResultReturn(json_obj, "intermediate_size"); if (intermediate_size_res.IsOk()) { config.intermediate_size = static_cast(intermediate_size_res.Unwrap()); } Result num_attention_heads_res = json::LookupWithResultReturn(json_obj, "num_attention_heads"); if (num_attention_heads_res.IsOk()) { config.num_attention_heads = static_cast(num_attention_heads_res.Unwrap()); } Result num_hidden_layers_res = json::LookupWithResultReturn(json_obj, "num_hidden_layers"); if (num_hidden_layers_res.IsOk()) { config.num_hidden_layers = static_cast(num_hidden_layers_res.Unwrap()); } Result patch_size_res = json::LookupWithResultReturn(json_obj, "patch_size"); if (patch_size_res.IsOk()) { config.patch_size = static_cast(patch_size_res.Unwrap()); } Result projection_dim_res = json::LookupWithResultReturn(json_obj, "projection_dim"); if (projection_dim_res.IsOk()) { config.projection_dim = static_cast(projection_dim_res.Unwrap()); } Result vocab_size_res = json::LookupWithResultReturn(json_obj, "vocab_size"); if (vocab_size_res.IsOk()) { config.vocab_size = static_cast(vocab_size_res.Unwrap()); } Result dtype_res = json::LookupWithResultReturn(json_obj, "dtype"); if (dtype_res.IsOk()) { config.dtype = dtype_res.Unwrap(); } Result num_channels_res = json::LookupWithResultReturn(json_obj, "num_channels"); if (num_channels_res.IsOk()) { config.num_channels = static_cast(num_channels_res.Unwrap()); } Result layer_norm_eps_res = json::LookupWithResultReturn(json_obj, "layer_norm_eps"); if (layer_norm_eps_res.IsOk()) { config.layer_norm_eps = layer_norm_eps_res.Unwrap(); } return config; } /****************** Model config ******************/ ModelConfig ModelConfig::FromJSON(const tvm::ffi::json::Object& json_obj) { ModelConfig config; Result vocab_size_res = json::LookupWithResultReturn(json_obj, "vocab_size"); if (vocab_size_res.IsOk()) { config.vocab_size = static_cast(vocab_size_res.Unwrap()); } Result context_window_size_res = json::LookupWithResultReturn(json_obj, "context_window_size"); if (context_window_size_res.IsOk()) { config.context_window_size = static_cast(context_window_size_res.Unwrap()); } Result sliding_window_size_res = json::LookupWithResultReturn(json_obj, "sliding_window_size"); if (sliding_window_size_res.IsOk()) { config.sliding_window_size = static_cast(sliding_window_size_res.Unwrap()); } Result prefill_chunk_size_res = json::LookupWithResultReturn(json_obj, "prefill_chunk_size"); if (prefill_chunk_size_res.IsOk()) { config.prefill_chunk_size = static_cast(prefill_chunk_size_res.Unwrap()); } Result tensor_parallel_shards_res = json::LookupWithResultReturn(json_obj, "tensor_parallel_shards"); if (tensor_parallel_shards_res.IsOk()) { config.tensor_parallel_shards = static_cast(tensor_parallel_shards_res.Unwrap()); } Result pipeline_parallel_stages_res = json::LookupWithResultReturn(json_obj, "pipeline_parallel_stages"); if (pipeline_parallel_stages_res.IsOk()) { config.pipeline_parallel_stages = static_cast(pipeline_parallel_stages_res.Unwrap()); } Result max_batch_size_res = json::LookupWithResultReturn(json_obj, "max_batch_size"); if (max_batch_size_res.IsOk()) { config.max_batch_size = static_cast(max_batch_size_res.Unwrap()); } if (json_obj.count("vision_config")) { const tvm::ffi::json::Object& vision_config_obj = json_obj.at("vision_config").cast(); config.vision_config = ModelVisionConfig::FromJSON(vision_config_obj); } return config; } /****************** Conversation template ******************/ std::unordered_map PLACEHOLDERS = { {MessagePlaceholders::SYSTEM, "{system_message}"}, {MessagePlaceholders::USER, "{user_message}"}, {MessagePlaceholders::ASSISTANT, "{assistant_message}"}, {MessagePlaceholders::TOOL, "{tool_message}"}, {MessagePlaceholders::FUNCTION, "{function_string}"}}; MessagePlaceholders MessagePlaceholderFromString(const std::string& role) { static const std::unordered_map enum_map = { {"system", MessagePlaceholders::SYSTEM}, {"user", MessagePlaceholders::USER}, {"assistant", MessagePlaceholders::ASSISTANT}, {"tool", MessagePlaceholders::TOOL}, {"function", MessagePlaceholders::FUNCTION}, }; return enum_map.at(role); } Conversation::Conversation() : role_templates({{"user", PLACEHOLDERS[MessagePlaceholders::USER]}, {"assistant", PLACEHOLDERS[MessagePlaceholders::ASSISTANT]}, {"tool", PLACEHOLDERS[MessagePlaceholders::TOOL]}}) {} std::string Conversation::GetSystemText(const std::string& system_msg) const { std::string system_text = this->system_template; static std::string system_placeholder = PLACEHOLDERS[MessagePlaceholders::SYSTEM]; size_t pos = system_text.find(system_placeholder); if (pos != std::string::npos) { system_text.replace(pos, system_placeholder.length(), system_msg); } return system_text; } std::string Conversation::GetRoleText(const std::string& role, const std::string& content, const std::optional& fn_call_string) const { std::string role_text = this->role_templates.at(role); std::string placeholder = PLACEHOLDERS[MessagePlaceholderFromString(role)]; size_t pos = role_text.find(placeholder); if (pos != std::string::npos) { role_text.replace(pos, placeholder.length(), content); } if (fn_call_string) { // replace placeholder[FUNCTION] with function_string // this assumes function calling is used for a single request scenario only pos = role_text.find(PLACEHOLDERS[MessagePlaceholders::FUNCTION]); if (pos != std::string::npos) { role_text.replace(pos, PLACEHOLDERS[MessagePlaceholders::FUNCTION].length(), fn_call_string.value()); } } return role_text; } /// Try to detect if function calling is needed, if so, return the function calling string Result> TryGetFunctionCallingString( const Conversation& conv, const ChatCompletionRequest& request) { using TResult = Result>; if (!request.tools.has_value() || (request.tool_choice.has_value() && request.tool_choice.value() == "none")) { return TResult::Ok(std::nullopt); } std::vector tools_ = request.tools.value(); std::string tool_choice_ = request.tool_choice.value(); // TODO: support with tool choice as dict for (const auto& tool : tools_) { if (tool.function.name == tool_choice_) { tvm::ffi::json::Value function_str(tool.function.AsJSON()); return TResult::Ok(tvm::ffi::json::Stringify(function_str)); } } if (tool_choice_ != "auto") { return TResult::Error("Invalid tool_choice value in the request: " + tool_choice_); } tvm::ffi::json::Array function_list; for (const auto& tool : tools_) { function_list.push_back(tool.function.AsJSON()); } tvm::ffi::json::Value function_list_json(function_list); return TResult::Ok(tvm::ffi::json::Stringify(function_list_json)); }; Result> CreatePrompt(const Conversation& conv, const ChatCompletionRequest& request, const ModelConfig& config, DLDevice device) { using TResult = Result>; Result> fn_call_str_tmp = TryGetFunctionCallingString(conv, request); if (fn_call_str_tmp.IsErr()) { return TResult::Error(fn_call_str_tmp.UnwrapErr()); } std::optional fn_call_string = fn_call_str_tmp.Unwrap(); // Handle system message // concz bool has_custom_system = false; std::string custom_system_inputs; auto f_populate_system_message = [&](const std::vector& msg_vec) { for (ChatCompletionMessage msg : msg_vec) { if (msg.role == "system") { TVM_FFI_ICHECK(msg.content.IsText()) << "System message must be text"; custom_system_inputs += msg.content.Text(); has_custom_system = true; } } }; // go through messages in template and passed in. f_populate_system_message(conv.messages); f_populate_system_message(request.messages); // pending text records the text to be put into data // we lazily accumulate the pending text // to reduce amount of segments in the Data vector std::string pending_text = conv.GetSystemText(has_custom_system ? custom_system_inputs : conv.system_message); // Get the message strings std::vector message_list; size_t non_system_msg_count = 0; // returns error if error happens auto f_process_messages = [&](const std::vector& msg_vec) -> std::optional { for (size_t i = 0; i < msg_vec.size(); ++i) { const ChatCompletionMessage& msg = msg_vec[i]; // skip system message as it is already processed if (msg.role == "system") continue; auto role_it = conv.roles.find(msg.role); if (role_it == conv.roles.end()) { return TResult::Error("Role \"" + msg.role + "\" is not supported"); } const std::string& role_name = role_it->second; // skip when content is empty if (msg.content.IsNull()) { pending_text += role_name + conv.role_empty_sep; continue; } ++non_system_msg_count; // assistant uses conv.seps[1] if there are two seps int sep_offset = msg.role == "assistant" ? 1 : 0; const std::string& seperator = conv.seps[sep_offset % conv.seps.size()]; // setup role prefix std::string role_prefix = ""; // Do not append role prefix if this is the first message and there is already a system // message if (conv.add_role_after_system_message || pending_text.empty() || non_system_msg_count != 1) { role_prefix = role_name + conv.role_content_sep; } pending_text += role_prefix; if (msg.content.IsParts()) { for (const auto& item : msg.content.Parts()) { auto it_type = item.find("type"); if (it_type == item.end()) { return TResult::Error("The content of a message does not have \"type\" field"); } if (it_type->second == "text") { auto it_text = item.find("text"); if (it_text == item.end()) { return TResult::Error( "The text type content of a message does not have \"text\" field"); } // replace placeholder[ROLE] with input message from role pending_text += conv.GetRoleText(msg.role, it_text->second, fn_call_string); } else if (it_type->second == "image_url") { if (item.find("image_url") == item.end()) { return TResult::Error("Content should have an image_url field"); } std::string image_url = item.at("image_url"); // TODO(mlc-team): According to OpenAI API reference this // should be a map, with a "url" key containing the URL, but // we are just assuming this as the URL for now std::string base64_image = image_url.substr(image_url.find(",") + 1); Result image_data_res = LoadImageFromBase64(base64_image); if (image_data_res.IsErr()) { return TResult::Error(image_data_res.UnwrapErr()); } if (!config.vision_config.has_value()) { return TResult::Error("Vision config is required for image input"); } int image_size = config.vision_config.value().image_size; int patch_size = config.vision_config.value().patch_size; int embed_size = (image_size * image_size) / (patch_size * patch_size); Tensor image_data = image_data_res.Unwrap(); std::vector new_shape = {1, image_size, image_size, 3}; Tensor image_tensor = image_data.CreateView(new_shape, image_data.DataType()); // TODO: Not sure if commenting will affect other functions. But // python part will do clip preprocessing. auto image_tensor = // ClipPreprocessor(image_data_res.Unwrap(), image_size, device); // lazily commit text data if (pending_text.length() != 0) { message_list.push_back(TextData(pending_text)); pending_text = ""; } message_list.push_back(ImageData(image_tensor, embed_size)); } else { return TResult::Error("Unsupported content type: " + it_type->second); } } } else { TVM_FFI_ICHECK(msg.content.IsText()); pending_text += conv.GetRoleText(msg.role, msg.content.Text(), fn_call_string); } pending_text += seperator; } return std::nullopt; }; // Optionally strip `...` blocks from historical assistant // messages (those before the last user message), mirroring Qwen3's HF chat // template. See mlc-ai/mlc-llm#3482. const std::vector* conv_messages_ptr = &conv.messages; const std::vector* request_messages_ptr = &request.messages; std::vector stripped_conv_messages; std::vector stripped_request_messages; if (conv.strip_reasoning_in_history) { const size_t conv_size = conv.messages.size(); int64_t last_user_idx = -1; for (size_t i = 0; i < conv_size; ++i) { if (conv.messages[i].role == "user") last_user_idx = static_cast(i); } for (size_t i = 0; i < request.messages.size(); ++i) { if (request.messages[i].role == "user") last_user_idx = static_cast(conv_size + i); } const std::string kCloseTag = ""; auto strip_range = [&](const std::vector& in, size_t offset, std::vector& out) { out.reserve(in.size()); for (size_t i = 0; i < in.size(); ++i) { ChatCompletionMessage msg = in[i]; const int64_t global_idx = static_cast(offset + i); if (msg.role == "assistant" && global_idx < last_user_idx && msg.content.IsText()) { const std::string& text = msg.content.Text(); const size_t close_pos = text.rfind(kCloseTag); if (close_pos != std::string::npos) { size_t start = close_pos + kCloseTag.size(); while (start < text.size() && text[start] == '\n') ++start; msg.content = ChatCompletionMessageContent(text.substr(start)); } } out.push_back(std::move(msg)); } }; strip_range(conv.messages, 0, stripped_conv_messages); strip_range(request.messages, conv_size, stripped_request_messages); conv_messages_ptr = &stripped_conv_messages; request_messages_ptr = &stripped_request_messages; } if (auto err = f_process_messages(*conv_messages_ptr)) { return err.value(); } if (auto err = f_process_messages(*request_messages_ptr)) { return err.value(); } // append last assistant begin message ChatCompletionMessage last_assistant_begin; last_assistant_begin.role = "assistant"; last_assistant_begin.content = std::nullopt; if (auto err = f_process_messages({last_assistant_begin})) { return err.value(); } if (pending_text.length() != 0) { message_list.push_back(TextData(pending_text)); } // Handle system_prefix_token_ids if (conv.system_prefix_token_ids.has_value()) { message_list.insert(message_list.begin(), TokenData(conv.system_prefix_token_ids.value())); } return TResult::Ok(message_list); } Result Conversation::FromJSON(const tvm::ffi::json::Object& json_obj) { using TResult = Result; Conversation conv; Result> name_res = json::LookupOptionalWithResultReturn(json_obj, "name"); if (name_res.IsErr()) { return TResult::Error(name_res.UnwrapErr()); } conv.name = name_res.Unwrap(); Result system_template_res = json::LookupWithResultReturn(json_obj, "system_template"); if (system_template_res.IsErr()) { return TResult::Error(system_template_res.UnwrapErr()); } conv.system_template = system_template_res.Unwrap(); Result system_message_res = json::LookupWithResultReturn(json_obj, "system_message"); if (system_message_res.IsErr()) { return TResult::Error(system_message_res.UnwrapErr()); } conv.system_message = system_message_res.Unwrap(); Result> system_prefix_token_ids_arr_res = json::LookupOptionalWithResultReturn(json_obj, "system_prefix_token_ids"); if (system_prefix_token_ids_arr_res.IsErr()) { return TResult::Error(system_prefix_token_ids_arr_res.UnwrapErr()); } std::optional system_prefix_token_ids_arr = system_prefix_token_ids_arr_res.Unwrap(); if (system_prefix_token_ids_arr.has_value()) { std::vector system_prefix_token_ids; system_prefix_token_ids.reserve(system_prefix_token_ids_arr.value().size()); for (const auto& token_id : system_prefix_token_ids_arr.value()) { if (!token_id.try_cast().has_value()) { return TResult::Error("A system prefix token id is not integer."); } system_prefix_token_ids.push_back(static_cast(token_id.cast())); } conv.system_prefix_token_ids = std::move(system_prefix_token_ids); } Result add_role_after_system_message_res = json::LookupWithResultReturn(json_obj, "add_role_after_system_message"); if (add_role_after_system_message_res.IsErr()) { return TResult::Error(add_role_after_system_message_res.UnwrapErr()); } conv.add_role_after_system_message = add_role_after_system_message_res.Unwrap(); Result roles_object_res = json::LookupWithResultReturn(json_obj, "roles"); if (roles_object_res.IsErr()) { return TResult::Error(roles_object_res.UnwrapErr()); } for (const auto& role : roles_object_res.Unwrap()) { if (!role.second.try_cast().has_value()) { return TResult::Error("A role value in the conversation template is not a string."); } conv.roles[role.first.cast()] = role.second.cast(); } Result> role_templates_object_res = json::LookupOptionalWithResultReturn(json_obj, "role_templates"); if (role_templates_object_res.IsErr()) { return TResult::Error(role_templates_object_res.UnwrapErr()); } std::optional role_templates_object = role_templates_object_res.Unwrap(); if (role_templates_object.has_value()) { for (const auto& [role, msg] : role_templates_object.value()) { if (!msg.try_cast().has_value()) { return TResult::Error("A value in \"role_templates\" is not a string."); } conv.role_templates[role.cast()] = msg.cast(); } } Result messages_arr_res = json::LookupWithResultReturn(json_obj, "messages"); if (messages_arr_res.IsErr()) { return TResult::Error(messages_arr_res.UnwrapErr()); } for (const auto& message : messages_arr_res.Unwrap()) { if (!message.try_cast().has_value() || message.cast().size() != 2) { return TResult::Error( "A message in the conversation template is not an array of [role, content]."); } tvm::ffi::json::Array message_arr = message.cast(); if (!message_arr[0].try_cast().has_value()) { return TResult::Error("The role of a message in the conversation template is not a string."); } std::string role = message_arr[0].cast(); // content can be a string or an array of objects if (message_arr[1].try_cast().has_value()) { ChatCompletionMessage msg; msg.role = role; msg.content = message_arr[1].cast(); conv.messages.push_back(msg); continue; } else if (message_arr[1].try_cast().has_value()) { tvm::ffi::json::Array content_arr = message_arr[1].cast(); std::vector> content; content.reserve(content_arr.size()); for (const auto& item : content_arr) { if (!item.try_cast().has_value()) { return TResult::Error("The content of conversation template message is not an object"); } std::unordered_map item_map; for (const auto& [key, value] : item.cast()) { item_map[key.cast()] = tvm::ffi::json::Stringify(value); } content.push_back(std::move(item_map)); } ChatCompletionMessage msg; msg.role = role; msg.content = content; conv.messages.push_back(msg); continue; } else { return TResult::Error( "The content of a message in the conversation template is not a string or an array."); } } Result seps_arr_res = json::LookupWithResultReturn(json_obj, "seps"); if (seps_arr_res.IsErr()) { return TResult::Error(seps_arr_res.UnwrapErr()); } std::vector seps; for (const auto& sep : seps_arr_res.Unwrap()) { if (!sep.try_cast().has_value()) { return TResult::Error("A separator (\"seps\") of the conversation template is not a string"); } conv.seps.push_back(sep.cast()); } Result role_content_sep_res = json::LookupWithResultReturn(json_obj, "role_content_sep"); if (role_content_sep_res.IsErr()) { return TResult::Error(role_content_sep_res.UnwrapErr()); } conv.role_content_sep = role_content_sep_res.Unwrap(); Result role_empty_sep_res = json::LookupWithResultReturn(json_obj, "role_empty_sep"); if (role_empty_sep_res.IsErr()) { return TResult::Error(role_empty_sep_res.UnwrapErr()); } conv.role_empty_sep = role_empty_sep_res.Unwrap(); Result stop_str_arr_res = json::LookupWithResultReturn(json_obj, "stop_str"); if (stop_str_arr_res.IsErr()) { return TResult::Error(stop_str_arr_res.UnwrapErr()); } for (const auto& stop : stop_str_arr_res.Unwrap()) { if (!stop.try_cast().has_value()) { return TResult::Error( "A stop string (\"stop_str\") of the conversation template is not a string."); } conv.stop_str.push_back(stop.cast()); } Result stop_token_ids_arr_res = json::LookupWithResultReturn(json_obj, "stop_token_ids"); if (stop_token_ids_arr_res.IsErr()) { return TResult::Error(stop_token_ids_arr_res.UnwrapErr()); } for (const auto& stop : stop_token_ids_arr_res.Unwrap()) { if (!stop.try_cast().has_value()) { return TResult::Error( "A stop token id (\"stop_token_ids\") of the conversation template is not an integer."); } conv.stop_token_ids.push_back(static_cast(stop.cast())); } Result> strip_reasoning_res = json::LookupOptionalWithResultReturn(json_obj, "strip_reasoning_in_history"); if (strip_reasoning_res.IsErr()) { return TResult::Error(strip_reasoning_res.UnwrapErr()); } conv.strip_reasoning_in_history = strip_reasoning_res.Unwrap().value_or(false); return TResult::Ok(conv); } Result Conversation::FromJSON(const std::string& json_str) { Result json_obj = json::ParseToJSONObjectWithResultReturn(json_str); if (json_obj.IsErr()) { return Result::Error(json_obj.UnwrapErr()); } return Conversation::FromJSON(json_obj.Unwrap()); } } // namespace json_ffi } // namespace llm } // namespace mlc