From 2f00afd1ea6a0aec3072a05c06a9d1e83f02a9f7 Mon Sep 17 00:00:00 2001 From: Tarek Dakhran Date: Fri, 13 Feb 2026 16:10:48 +0100 Subject: [PATCH 1/4] mtmd : chat : Fix extra \n between text and media marker Thanks to @tugot17 for detecting and reporting the issue. For vision models (e.g. LFM2.5-VL-1.6B and Qwen/Qwen3-VL-4B-Instruct) `llama-mtmd-cli` produces identical output to HF implementation. However `llama-server` doesn't. I traced it down to extra newline inserted after `<__media__>`. This happens in `to_json_oaicompat`, that treats media markers as text and joins all parts with `\n` separator. PR introduces new type `media_marker` and uses it for media markers. Extra logic is added to prevent insertion of newlines before and after media markers. With this change number of input tokens is identical to HF implementation and as a result the output is also identical. I explored other ways to address the issue * remove completely `\n` between text parts in `to_json_oaicompat` * merge text messages in server-common.cpp before sending them to `to_json_oaicompat` Please propose alternative ways of fixing this issue. --- common/chat.cpp | 11 +++++++---- tools/server/server-common.cpp | 6 ++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index 47a34d58228f..f8f3a0a79715 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -65,15 +65,18 @@ json common_chat_msg::to_json_oaicompat(bool concat_typed_text) const { } else if (!content_parts.empty()) { if (concat_typed_text) { std::string text; + bool last_was_media_marker = false; + // join parts with newline, do not add newline before or after media markers for (const auto & part : content_parts) { - if (part.type != "text") { + if (part.type != "text" && part.type != "media_marker") { LOG_WRN("Ignoring content part type: %s\n", part.type.c_str()); continue; } - if (!text.empty()) { + if (part.type != "media_marker" && !last_was_media_marker && !text.empty()) { text += '\n'; } - text += part.text; + last_was_media_marker = (part.type == "media_marker"); + text += part.text; } jmsg["content"] = text; } else { @@ -319,7 +322,7 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa throw std::invalid_argument("Missing content part type: " + part.dump()); } const auto & type = part.at("type"); - if (type != "text") { + if (type != "text" && type != "media_marker") { throw std::invalid_argument("Unsupported content part type: " + type.dump()); } common_chat_msg_content_part msg_part; diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index a853f65c8d8f..d717fb6698fe 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -916,8 +916,7 @@ json oaicompat_chat_params_parse( json image_url = json_value(p, "image_url", json::object()); handle_media(out_files, image_url, opt.media_path); - // replace this chunk with a marker - p["type"] = "text"; + p["type"] = "media_marker"; p["text"] = mtmd_default_marker(); p.erase("image_url"); @@ -938,8 +937,7 @@ json oaicompat_chat_params_parse( // TODO: add audio_url support by reusing handle_media() - // replace this chunk with a marker - p["type"] = "text"; + p["type"] = "media_marker"; p["text"] = mtmd_default_marker(); p.erase("input_audio"); From b642c339fe1a4c9baedda6207fdc2fb78e75385d Mon Sep 17 00:00:00 2001 From: Tarek Dakhran Date: Tue, 17 Feb 2026 14:41:22 +0100 Subject: [PATCH 2/4] Refactor to use explicite per type ifs --- common/chat.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index f8f3a0a79715..11dfa71231c3 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -68,14 +68,22 @@ json common_chat_msg::to_json_oaicompat(bool concat_typed_text) const { bool last_was_media_marker = false; // join parts with newline, do not add newline before or after media markers for (const auto & part : content_parts) { - if (part.type != "text" && part.type != "media_marker") { + bool add_new_line = true; + if (part.type == "text") { + add_new_line = !last_was_media_marker && !text.empty(); + last_was_media_marker = false; + } else if (part.type == "media_marker") { + add_new_line = false; + last_was_media_marker = true; + } else { LOG_WRN("Ignoring content part type: %s\n", part.type.c_str()); continue; } - if (part.type != "media_marker" && !last_was_media_marker && !text.empty()) { + + if (add_new_line) { text += '\n'; } - last_was_media_marker = (part.type == "media_marker"); + text += part.text; } jmsg["content"] = text; From de2ae138217a83b920fd514e0b6ba9940d950143 Mon Sep 17 00:00:00 2001 From: Tarek Dakhran Date: Tue, 17 Feb 2026 15:37:43 +0100 Subject: [PATCH 3/4] Update common/chat.cpp Co-authored-by: Piotr Wilkin (ilintar) --- common/chat.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/chat.cpp b/common/chat.cpp index 11dfa71231c3..3ebf811bf87f 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -84,7 +84,7 @@ json common_chat_msg::to_json_oaicompat(bool concat_typed_text) const { text += '\n'; } - text += part.text; + text += part.text; } jmsg["content"] = text; } else { From 8be2896e728d5ae3055cb555fd2afd6460f9d0b7 Mon Sep 17 00:00:00 2001 From: Tarek Dakhran Date: Tue, 17 Feb 2026 16:35:27 +0100 Subject: [PATCH 4/4] Update common_chat_templates_apply_legacy --- common/chat.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/chat.cpp b/common/chat.cpp index 3ebf811bf87f..3c4e9f5cf0cd 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -3318,7 +3318,7 @@ static common_chat_params common_chat_templates_apply_legacy( for (const auto & msg : inputs.messages) { auto content = msg.content; for (const auto & part : msg.content_parts) { - if (part.type != "text") { + if (part.type != "text" && part.type != "media_marker") { LOG_WRN("Ignoring non-text content part: %s\n", part.type.c_str()); continue; }