Commit bd4eeaa04 for llama.cpp
commit bd4eeaa047006cb1fe71999fbd11134b5836e167
Author: Nicolas Budyn <16596345+nbud@users.noreply.github.com>
Date: Wed Oct 7 22:48:06 2026 +0200
chat: fix jinja parser for TranslateGemma (#30096)
* chat: fix jinja parser for translategemma
* log if missing soruce_lang_code or target_lang_code
diff --git a/common/chat.cpp b/common/chat.cpp
index 2c795c281..c8f156a70 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -1223,6 +1223,13 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
return common_chat_params_init_minicpm5(tmpl, params);
}
+ // TranslateGemma - user content must follow a custom schema with language codes
+ if (src.find("[source_lang_code]") != std::string::npos &&
+ src.find("[target_lang_code]") != std::string::npos) {
+ LOG_DBG("Using specialized template: TranslateGemma\n");
+ return common_chat_params_init_translate_gemma(tmpl, params);
+ }
+
// Qwen3-Coder XML tool calls, also used by Nemotron Nano 3, Qwen3.5 and StepFun-3.5-Flash
if (src.find("<tool_call>") != std::string::npos &&
src.find("<function=") != std::string::npos &&
diff --git a/common/parsers/parsers.h b/common/parsers/parsers.h
index 1b385264e..fd9fc1ce1 100644
--- a/common/parsers/parsers.h
+++ b/common/parsers/parsers.h
@@ -81,3 +81,5 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat
common_chat_params common_chat_params_init_muse_glimmer(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
common_chat_params common_chat_params_init_qwen3_coder(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
+
+common_chat_params common_chat_params_init_translate_gemma(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
diff --git a/common/parsers/sources.cmake b/common/parsers/sources.cmake
index af0c68019..152397074 100644
--- a/common/parsers/sources.cmake
+++ b/common/parsers/sources.cmake
@@ -20,4 +20,5 @@ set(LLAMA_CHAT_PARSERS_SOURCES
${CMAKE_CURRENT_LIST_DIR}/ministral3.cpp
${CMAKE_CURRENT_LIST_DIR}/muse-glimmer.cpp
${CMAKE_CURRENT_LIST_DIR}/qwen3-coder.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/translate-gemma.cpp
)
diff --git a/common/parsers/translate-gemma.cpp b/common/parsers/translate-gemma.cpp
new file mode 100644
index 000000000..c52778064
--- /dev/null
+++ b/common/parsers/translate-gemma.cpp
@@ -0,0 +1,63 @@
+#include "parsers.h"
+#include "log.h"
+
+// TranslateGemma does not support tools or reasoning, it only needs user messages in its own content schema
+common_chat_params common_chat_params_init_translate_gemma(
+ const common_chat_template & tmpl,
+ const autoparser::generation_params & inputs) {
+
+ common_chat_params data;
+
+ // default to chat_template_kwargs, or en-GB if not specified
+ std::string src_lang = inputs.extra_context.value("source_lang_code", "en-GB");
+ std::string tgt_lang = inputs.extra_context.value("target_lang_code", "en-GB");
+ for (const char * key : { "source_lang_code", "target_lang_code" }) {
+ if (!inputs.extra_context.contains(key)) {
+ LOG_WRN("TranslateGemma: %s not set in chat_template_kwargs, defaulting to en-GB\n", key);
+ }
+ }
+
+ json messages = inputs.messages;
+ for (auto & message : messages) {
+ if (message.value("role", "") != "user") {
+ continue;
+ }
+ std::string text;
+ const auto & content = message.contains("content") ? message.at("content") : json();
+ if (content.is_string()) {
+ text = content.get<std::string>();
+ } else if (content.is_array()) {
+ for (const auto & part : content) {
+ if (!text.empty()) {
+ text += "\n";
+ }
+ text += part.value("text", "");
+ }
+ }
+ message["content"] = json::array({
+ json{
+ {"type", "text"},
+ {"text", text},
+ {"source_lang_code", src_lang},
+ {"target_lang_code", tgt_lang},
+ }
+ });
+ }
+
+ data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs, messages);
+ data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs, messages);
+ data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
+ data.supports_thinking = false;
+
+ if (inputs.has_continuation()) {
+ data.generation_prompt = "<start_of_turn>model\n" + inputs.continue_msg.render_content();
+ data.prompt += data.generation_prompt;
+ }
+
+ auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
+ return p.literal(data.generation_prompt) << p.content(p.rest());
+ });
+ data.parser = parser.save();
+
+ return data;
+}
diff --git a/models/templates/google-translategemma-4b-it.jinja b/models/templates/google-translategemma-4b-it.jinja
new file mode 100644
index 000000000..d571968f9
--- /dev/null
+++ b/models/templates/google-translategemma-4b-it.jinja
@@ -0,0 +1,641 @@
+{%- set languages = {
+ "aa": "Afar",
+ "aa-DJ": "Afar",
+ "aa-ER": "Afar",
+ "ab": "Abkhazian",
+ "af": "Afrikaans",
+ "af-NA": "Afrikaans",
+ "ak": "Akan",
+ "am": "Amharic",
+ "an": "Aragonese",
+ "ar": "Arabic",
+ "ar-AE": "Arabic",
+ "ar-BH": "Arabic",
+ "ar-DJ": "Arabic",
+ "ar-DZ": "Arabic",
+ "ar-EG": "Arabic",
+ "ar-EH": "Arabic",
+ "ar-ER": "Arabic",
+ "ar-IL": "Arabic",
+ "ar-IQ": "Arabic",
+ "ar-JO": "Arabic",
+ "ar-KM": "Arabic",
+ "ar-KW": "Arabic",
+ "ar-LB": "Arabic",
+ "ar-LY": "Arabic",
+ "ar-MA": "Arabic",
+ "ar-MR": "Arabic",
+ "ar-OM": "Arabic",
+ "ar-PS": "Arabic",
+ "ar-QA": "Arabic",
+ "ar-SA": "Arabic",
+ "ar-SD": "Arabic",
+ "ar-SO": "Arabic",
+ "ar-SS": "Arabic",
+ "ar-SY": "Arabic",
+ "ar-TD": "Arabic",
+ "ar-TN": "Arabic",
+ "ar-YE": "Arabic",
+ "as": "Assamese",
+ "az": "Azerbaijani",
+ "az-Arab": "Azerbaijani",
+ "az-Arab-IQ": "Azerbaijani",
+ "az-Arab-TR": "Azerbaijani",
+ "az-Cyrl": "Azerbaijani",
+ "az-Latn": "Azerbaijani",
+ "ba": "Bashkir",
+ "be": "Belarusian",
+ "be-tarask": "Belarusian",
+ "bg": "Bulgarian",
+ "bg-BG": "Bulgarian",
+ "bm": "Bambara",
+ "bm-Nkoo": "Bambara",
+ "bn": "Bengali",
+ "bn-IN": "Bengali",
+ "bo": "Tibetan",
+ "bo-IN": "Tibetan",
+ "br": "Breton",
+ "bs": "Bosnian",
+ "bs-Cyrl": "Bosnian",
+ "bs-Latn": "Bosnian",
+ "ca": "Catalan",
+ "ca-AD": "Catalan",
+ "ca-ES": "Catalan",
+ "ca-FR": "Catalan",
+ "ca-IT": "Catalan",
+ "ce": "Chechen",
+ "co": "Corsican",
+ "cs": "Czech",
+ "cs-CZ": "Czech",
+ "cv": "Chuvash",
+ "cy": "Welsh",
+ "da": "Danish",
+ "da-DK": "Danish",
+ "da-GL": "Danish",
+ "de": "German",
+ "de-AT": "German",
+ "de-BE": "German",
+ "de-CH": "German",
+ "de-DE": "German",
+ "de-IT": "German",
+ "de-LI": "German",
+ "de-LU": "German",
+ "dv": "Divehi",
+ "dz": "Dzongkha",
+ "ee": "Ewe",
+ "ee-TG": "Ewe",
+ "el": "Greek",
+ "el-CY": "Greek",
+ "el-GR": "Greek",
+ "el-polyton": "Greek",
+ "en": "English",
+ "en-AE": "English",
+ "en-AG": "English",
+ "en-AI": "English",
+ "en-AS": "English",
+ "en-AT": "English",
+ "en-AU": "English",
+ "en-BB": "English",
+ "en-BE": "English",
+ "en-BI": "English",
+ "en-BM": "English",
+ "en-BS": "English",
+ "en-BW": "English",
+ "en-BZ": "English",
+ "en-CA": "English",
+ "en-CC": "English",
+ "en-CH": "English",
+ "en-CK": "English",
+ "en-CM": "English",
+ "en-CX": "English",
+ "en-CY": "English",
+ "en-CZ": "English",
+ "en-DE": "English",
+ "en-DG": "English",
+ "en-DK": "English",
+ "en-DM": "English",
+ "en-ER": "English",
+ "en-ES": "English",
+ "en-FI": "English",
+ "en-FJ": "English",
+ "en-FK": "English",
+ "en-FM": "English",
+ "en-FR": "English",
+ "en-GB": "English",
+ "en-GD": "English",
+ "en-GG": "English",
+ "en-GH": "English",
+ "en-GI": "English",
+ "en-GM": "English",
+ "en-GS": "English",
+ "en-GU": "English",
+ "en-GY": "English",
+ "en-HK": "English",
+ "en-HU": "English",
+ "en-ID": "English",
+ "en-IE": "English",
+ "en-IL": "English",
+ "en-IM": "English",
+ "en-IN": "English",
+ "en-IO": "English",
+ "en-IT": "English",
+ "en-JE": "English",
+ "en-JM": "English",
+ "en-KE": "English",
+ "en-KI": "English",
+ "en-KN": "English",
+ "en-KY": "English",
+ "en-LC": "English",
+ "en-LR": "English",
+ "en-LS": "English",
+ "en-MG": "English",
+ "en-MH": "English",
+ "en-MO": "English",
+ "en-MP": "English",
+ "en-MS": "English",
+ "en-MT": "English",
+ "en-MU": "English",
+ "en-MV": "English",
+ "en-MW": "English",
+ "en-MY": "English",
+ "en-NA": "English",
+ "en-NF": "English",
+ "en-NG": "English",
+ "en-NL": "English",
+ "en-NO": "English",
+ "en-NR": "English",
+ "en-NU": "English",
+ "en-NZ": "English",
+ "en-PG": "English",
+ "en-PH": "English",
+ "en-PK": "English",
+ "en-PL": "English",
+ "en-PN": "English",
+ "en-PR": "English",
+ "en-PT": "English",
+ "en-PW": "English",
+ "en-RO": "English",
+ "en-RW": "English",
+ "en-SB": "English",
+ "en-SC": "English",
+ "en-SD": "English",
+ "en-SE": "English",
+ "en-SG": "English",
+ "en-SH": "English",
+ "en-SI": "English",
+ "en-SK": "English",
+ "en-SL": "English",
+ "en-SS": "English",
+ "en-SX": "English",
+ "en-SZ": "English",
+ "en-TC": "English",
+ "en-TK": "English",
+ "en-TO": "English",
+ "en-TT": "English",
+ "en-TV": "English",
+ "en-TZ": "English",
+ "en-UG": "English",
+ "en-UM": "English",
+ "en-VC": "English",
+ "en-VG": "English",
+ "en-VI": "English",
+ "en-VU": "English",
+ "en-WS": "English",
+ "en-ZA": "English",
+ "en-ZM": "English",
+ "en-ZW": "English",
+ "eo": "Esperanto",
+ "es": "Spanish",
+ "es-AR": "Spanish",
+ "es-BO": "Spanish",
+ "es-BR": "Spanish",
+ "es-BZ": "Spanish",
+ "es-CL": "Spanish",
+ "es-CO": "Spanish",
+ "es-CR": "Spanish",
+ "es-CU": "Spanish",
+ "es-DO": "Spanish",
+ "es-EA": "Spanish",
+ "es-EC": "Spanish",
+ "es-ES": "Spanish",
+ "es-GQ": "Spanish",
+ "es-GT": "Spanish",
+ "es-HN": "Spanish",
+ "es-IC": "Spanish",
+ "es-MX": "Spanish",
+ "es-NI": "Spanish",
+ "es-PA": "Spanish",
+ "es-PE": "Spanish",
+ "es-PH": "Spanish",
+ "es-PR": "Spanish",
+ "es-PY": "Spanish",
+ "es-SV": "Spanish",
+ "es-US": "Spanish",
+ "es-UY": "Spanish",
+ "es-VE": "Spanish",
+ "et": "Estonian",
+ "et-EE": "Estonian",
+ "eu": "Basque",
+ "fa": "Persian",
+ "fa-AF": "Persian",
+ "fa-IR": "Persian",
+ "ff": "Fulah",
+ "ff-Adlm": "Fulah",
+ "ff-Adlm-BF": "Fulah",
+ "ff-Adlm-CM": "Fulah",
+ "ff-Adlm-GH": "Fulah",
+ "ff-Adlm-GM": "Fulah",
+ "ff-Adlm-GW": "Fulah",
+ "ff-Adlm-LR": "Fulah",
+ "ff-Adlm-MR": "Fulah",
+ "ff-Adlm-NE": "Fulah",
+ "ff-Adlm-NG": "Fulah",
+ "ff-Adlm-SL": "Fulah",
+ "ff-Adlm-SN": "Fulah",
+ "ff-Latn": "Fulah",
+ "ff-Latn-BF": "Fulah",
+ "ff-Latn-CM": "Fulah",
+ "ff-Latn-GH": "Fulah",
+ "ff-Latn-GM": "Fulah",
+ "ff-Latn-GN": "Fulah",
+ "ff-Latn-GW": "Fulah",
+ "ff-Latn-LR": "Fulah",
+ "ff-Latn-MR": "Fulah",
+ "ff-Latn-NE": "Fulah",
+ "ff-Latn-NG": "Fulah",
+ "ff-Latn-SL": "Fulah",
+ "fi": "Finnish",
+ "fi-FI": "Finnish",
+ "fil-PH": "Filipino",
+ "fo": "Faroese",
+ "fo-DK": "Faroese",
+ "fr": "French",
+ "fr-BE": "French",
+ "fr-BF": "French",
+ "fr-BI": "French",
+ "fr-BJ": "French",
+ "fr-BL": "French",
+ "fr-CA": "French",
+ "fr-CD": "French",
+ "fr-CF": "French",
+ "fr-CG": "French",
+ "fr-CH": "French",
+ "fr-CI": "French",
+ "fr-CM": "French",
+ "fr-DJ": "French",
+ "fr-DZ": "French",
+ "fr-FR": "French",
+ "fr-GA": "French",
+ "fr-GF": "French",
+ "fr-GN": "French",
+ "fr-GP": "French",
+ "fr-GQ": "French",
+ "fr-HT": "French",
+ "fr-KM": "French",
+ "fr-LU": "French",
+ "fr-MA": "French",
+ "fr-MC": "French",
+ "fr-MF": "French",
+ "fr-MG": "French",
+ "fr-ML": "French",
+ "fr-MQ": "French",
+ "fr-MR": "French",
+ "fr-MU": "French",
+ "fr-NC": "French",
+ "fr-NE": "French",
+ "fr-PF": "French",
+ "fr-PM": "French",
+ "fr-RE": "French",
+ "fr-RW": "French",
+ "fr-SC": "French",
+ "fr-SN": "French",
+ "fr-SY": "French",
+ "fr-TD": "French",
+ "fr-TG": "French",
+ "fr-TN": "French",
+ "fr-VU": "French",
+ "fr-WF": "French",
+ "fr-YT": "French",
+ "fy": "Western Frisian",
+ "ga": "Irish",
+ "ga-GB": "Irish",
+ "gd": "Scottish Gaelic",
+ "gl": "Galician",
+ "gn": "Guarani",
+ "gu": "Gujarati",
+ "gu-IN": "Gujarati",
+ "gv": "Manx",
+ "ha": "Hausa",
+ "ha-Arab": "Hausa",
+ "ha-Arab-SD": "Hausa",
+ "ha-GH": "Hausa",
+ "ha-NE": "Hausa",
+ "he": "Hebrew",
+ "he-IL": "Hebrew",
+ "hi": "Hindi",
+ "hi-IN": "Hindi",
+ "hi-Latn": "Hindi",
+ "hr": "Croatian",
+ "hr-BA": "Croatian",
+ "hr-HR": "Croatian",
+ "ht": "Haitian",
+ "hu": "Hungarian",
+ "hu-HU": "Hungarian",
+ "hy": "Armenian",
+ "ia": "Interlingua",
+ "id": "Indonesian",
+ "id-ID": "Indonesian",
+ "ie": "Interlingue",
+ "ig": "Igbo",
+ "ii": "Sichuan Yi",
+ "ik": "Inupiaq",
+ "io": "Ido",
+ "is": "Icelandic",
+ "it": "Italian",
+ "it-CH": "Italian",
+ "it-IT": "Italian",
+ "it-SM": "Italian",
+ "it-VA": "Italian",
+ "iu": "Inuktitut",
+ "iu-Latn": "Inuktitut",
+ "ja": "Japanese",
+ "ja-JP": "Japanese",
+ "jv": "Javanese",
+ "ka": "Georgian",
+ "ki": "Kikuyu",
+ "kk": "Kazakh",
+ "kk-Arab": "Kazakh",
+ "kk-Cyrl": "Kazakh",
+ "kk-KZ": "Kazakh",
+ "kl": "Kalaallisut",
+ "km": "Central Khmer",
+ "kn": "Kannada",
+ "kn-IN": "Kannada",
+ "ko": "Korean",
+ "ko-CN": "Korean",
+ "ko-KP": "Korean",
+ "ko-KR": "Korean",
+ "ks": "Kashmiri",
+ "ks-Arab": "Kashmiri",
+ "ks-Deva": "Kashmiri",
+ "ku": "Kurdish",
+ "kw": "Cornish",
+ "ky": "Kyrgyz",
+ "la": "Latin",
+ "lb": "Luxembourgish",
+ "lg": "Ganda",
+ "ln": "Lingala",
+ "ln-AO": "Lingala",
+ "ln-CF": "Lingala",
+ "ln-CG": "Lingala",
+ "lo": "Lao",
+ "lt": "Lithuanian",
+ "lt-LT": "Lithuanian",
+ "lu": "Luba-Katanga",
+ "lv": "Latvian",
+ "lv-LV": "Latvian",
+ "mg": "Malagasy",
+ "mi": "Maori",
+ "mk": "Macedonian",
+ "ml": "Malayalam",
+ "ml-IN": "Malayalam",
+ "mn": "Mongolian",
+ "mn-Mong": "Mongolian",
+ "mn-Mong-MN": "Mongolian",
+ "mr": "Marathi",
+ "mr-IN": "Marathi",
+ "ms": "Malay",
+ "ms-Arab": "Malay",
+ "ms-Arab-BN": "Malay",
+ "ms-BN": "Malay",
+ "ms-ID": "Malay",
+ "ms-SG": "Malay",
+ "mt": "Maltese",
+ "my": "Burmese",
+ "nb": "Norwegian Bokmål",
+ "nb-SJ": "Norwegian Bokmål",
+ "nd": "North Ndebele",
+ "ne": "Nepali",
+ "ne-IN": "Nepali",
+ "nl": "Dutch",
+ "nl-AW": "Dutch",
+ "nl-BE": "Dutch",
+ "nl-BQ": "Dutch",
+ "nl-CW": "Dutch",
+ "nl-NL": "Dutch",
+ "nl-SR": "Dutch",
+ "nl-SX": "Dutch",
+ "nn": "Norwegian Nynorsk",
+ "no": "Norwegian",
+ "no-NO": "Norwegian",
+ "nr": "South Ndebele",
+ "nv": "Navajo",
+ "ny": "Chichewa",
+ "oc": "Occitan",
+ "oc-ES": "Occitan",
+ "om": "Oromo",
+ "om-KE": "Oromo",
+ "or": "Oriya",
+ "os": "Ossetian",
+ "os-RU": "Ossetian",
+ "pa": "Punjabi",
+ "pa-IN": "Punjabi",
+ "pa-Arab": "Punjabi",
+ "pa-Guru": "Punjabi",
+ "pl": "Polish",
+ "pl-PL": "Polish",
+ "ps": "Pashto",
+ "ps-PK": "Pashto",
+ "pt": "Portuguese",
+ "pt-AO": "Portuguese",
+ "pt-BR": "Portuguese",
+ "pt-CH": "Portuguese",
+ "pt-CV": "Portuguese",
+ "pt-GQ": "Portuguese",
+ "pt-GW": "Portuguese",
+ "pt-LU": "Portuguese",
+ "pt-MO": "Portuguese",
+ "pt-MZ": "Portuguese",
+ "pt-PT": "Portuguese",
+ "pt-ST": "Portuguese",
+ "pt-TL": "Portuguese",
+ "qu": "Quechua",
+ "qu-BO": "Quechua",
+ "qu-EC": "Quechua",
+ "rm": "Romansh",
+ "rn": "Rundi",
+ "ro": "Romanian",
+ "ro-MD": "Romanian",
+ "ro-RO": "Romanian",
+ "ru": "Russian",
+ "ru-BY": "Russian",
+ "ru-KG": "Russian",
+ "ru-KZ": "Russian",
+ "ru-MD": "Russian",
+ "ru-RU": "Russian",
+ "ru-UA": "Russian",
+ "rw": "Kinyarwanda",
+ "sa": "Sanskrit",
+ "sc": "Sardinian",
+ "sd": "Sindhi",
+ "sd-Arab": "Sindhi",
+ "sd-Deva": "Sindhi",
+ "se": "Northern Sami",
+ "se-FI": "Northern Sami",
+ "se-SE": "Northern Sami",
+ "sg": "Sango",
+ "si": "Sinhala",
+ "sk": "Slovak",
+ "sk-SK": "Slovak",
+ "sl": "Slovenian",
+ "sl-SI": "Slovenian",
+ "sn": "Shona",
+ "so": "Somali",
+ "so-DJ": "Somali",
+ "so-ET": "Somali",
+ "so-KE": "Somali",
+ "sq": "Albanian",
+ "sq-MK": "Albanian",
+ "sq-XK": "Albanian",
+ "sr": "Serbian",
+ "sr-RS": "Serbian",
+ "sr-Cyrl": "Serbian",
+ "sr-Cyrl-BA": "Serbian",
+ "sr-Cyrl-ME": "Serbian",
+ "sr-Cyrl-XK": "Serbian",
+ "sr-Latn": "Serbian",
+ "sr-Latn-BA": "Serbian",
+ "sr-Latn-ME": "Serbian",
+ "sr-Latn-XK": "Serbian",
+ "ss": "Swati",
+ "ss-SZ": "Swati",
+ "st": "Southern Sotho",
+ "st-LS": "Southern Sotho",
+ "su": "Sundanese",
+ "su-Latn": "Sundanese",
+ "sv": "Swedish",
+ "sv-AX": "Swedish",
+ "sv-FI": "Swedish",
+ "sv-SE": "Swedish",
+ "sw": "Swahili",
+ "sw-CD": "Swahili",
+ "sw-KE": "Swahili",
+ "sw-TZ": "Swahili",
+ "sw-UG": "Swahili",
+ "ta": "Tamil",
+ "ta-IN": "Tamil",
+ "ta-LK": "Tamil",
+ "ta-MY": "Tamil",
+ "ta-SG": "Tamil",
+ "te": "Telugu",
+ "te-IN": "Telugu",
+ "tg": "Tajik",
+ "th": "Thai",
+ "th-TH": "Thai",
+ "ti": "Tigrinya",
+ "ti-ER": "Tigrinya",
+ "tk": "Turkmen",
+ "tl": "Tagalog",
+ "tn": "Tswana",
+ "tn-BW": "Tswana",
+ "to": "Tonga",
+ "tr": "Turkish",
+ "tr-CY": "Turkish",
+ "tr-TR": "Turkish",
+ "ts": "Tsonga",
+ "tt": "Tatar",
+ "ug": "Uyghur",
+ "uk": "Ukrainian",
+ "uk-UA": "Ukrainian",
+ "ur": "Urdu",
+ "ur-IN": "Urdu",
+ "ur-PK": "Urdu",
+ "uz": "Uzbek",
+ "uz-Arab": "Uzbek",
+ "uz-Cyrl": "Uzbek",
+ "uz-Latn": "Uzbek",
+ "ve": "Venda",
+ "vi": "Vietnamese",
+ "vi-VN": "Vietnamese",
+ "vo": "Volapük",
+ "wa": "Walloon",
+ "wo": "Wolof",
+ "xh": "Xhosa",
+ "yi": "Yiddish",
+ "yo": "Yoruba",
+ "yo-BJ": "Yoruba",
+ "za": "Zhuang",
+ "zh": "Chinese",
+ "zh-CH": "Chinese",
+ "zh-TW": "Chinese",
+ "zh-Hans": "Chinese",
+ "zh-Hans-HK": "Chinese",
+ "zh-Hans-MO": "Chinese",
+ "zh-Hans-MY": "Chinese",
+ "zh-Hans-SG": "Chinese",
+ "zh-Hant": "Chinese",
+ "zh-Hant-HK": "Chinese",
+ "zh-Hant-MO": "Chinese",
+ "zh-Hant-MY": "Chinese",
+ "zh-Latn": "Chinese",
+ "zu": "Zulu",
+ "zu-ZA": "Zulu",
+}
+-%}
+{{ bos_token }}
+{%- if (messages[0]['role'] != 'user') -%}
+ {{ raise_exception("Conversations must start with a user prompt.") }}
+{%- endif -%}
+{%- for message in messages -%}
+ {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}
+ {{ raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") }}
+ {%- endif -%}
+ {%- if (message['role'] == 'assistant') -%}
+ {%- if message['content'] is none or message['content'] is not string -%}
+ {{ raise_exception("Assistant role must provide content as a string") }}
+ {%- endif -%}
+ {{ '<start_of_turn>model\n'}}
+ {{ message["content"] | trim }}
+ {%- elif (message['role'] == 'user') -%}
+ {%- if message['content'] is none or message['content'] is not iterable or message['content'] | length != 1 -%}
+ {{ raise_exception(
+ "User role must provide `content` as an iterable with exactly one item. That item must be a " +
+ "`mapping(type:'text' | 'image', source_lang_code:string, target_lang_code:string, " +
+ "text:string | none, image:string | none)`."
+ ) }}
+ {%- endif -%}
+ {%- set content = message["content"][0] -%}
+ {%- set source_lang_code = content["source_lang_code"] | replace("_", "-") -%}
+ {%- set source_lang = languages[source_lang_code] -%}
+ {%- set target_lang_code = content["target_lang_code"] | replace("_", "-") -%}
+ {%- set target_lang = languages[target_lang_code] -%}
+ {{ '<start_of_turn>user\nYou are a professional ' + source_lang + ' (' + source_lang_code + ') to ' +
+ target_lang + ' (' + target_lang_code + ') translator. Your goal is to accurately convey the meaning and '
+ 'nuances of the original ' + source_lang + ' text while adhering to ' + target_lang + ' grammar, '
+ 'vocabulary, and cultural sensitivities.\n'
+ }}
+ {%- if content["type"] == 'text' -%}
+ {{
+ 'Produce only the ' + target_lang + ' translation, without any additional explanations or ' +
+ 'commentary. Please translate the following ' + source_lang + ' text into ' + target_lang + ':\n\n\n' +
+ content["text"] | trim
+ }}
+ {%- elif content["type"] == 'image' -%}
+ {{
+ 'Please translate the ' + source_lang + ' text in the provided image into ' + target_lang + '. ' +
+ 'Produce only the ' + target_lang + ' translation, without any additional explanations, ' +
+ 'alternatives or commentary. Focus only on the text, do not output where the text is located, ' +
+ 'surrounding objects or any other explanation about the picture. Ignore symbols, pictogram, and ' +
+ 'arrows!\n\n\n<start_of_image>'
+ }}
+ {%- else -%}
+ {{ raise_exception("User role `content` can contain either `text` or `image` data.") }}
+ {%- endif -%}
+ {%- else -%}
+ {{ raise_exception("Conversations must only contain user or assistant roles.") }}
+ {%- endif -%}
+ {{ '<end_of_turn>\n' }}
+{%- endfor -%}
+{%- if add_generation_prompt -%}
+ {{'<start_of_turn>model\n'}}
+{%- endif -%}
diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
index f0aec5bbe..41e4948c4 100644
--- a/tests/test-chat.cpp
+++ b/tests/test-chat.cpp
@@ -7192,6 +7192,13 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.run();
}
+ // TranslateGemma
+ {
+ // no reconstruction check, the template adds whitespace around assistant content
+ auto tst = peg_tester("models/templates/google-translategemma-4b-it.jinja", detailed_debug);
+ tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();
+ }
+
// MiniCPM5 - XML tool calls with <function name="..."><param name="...">...</param></function>
{
auto tst = peg_tester("models/templates/openbmb-MiniCPM5-1B.jinja", detailed_debug);
@@ -7609,6 +7616,43 @@ static void test_developer_role_to_system_workaround() {
}
}
+// TranslateGemma raises on plain string user content, the specialized handler must rewrite it
+static void test_translate_gemma() {
+ LOG_DBG("%s\n", __func__);
+
+ auto tmpls = read_templates("models/templates/google-translategemma-4b-it.jinja");
+
+ // server startup renders this example, it must not throw
+ auto example = common_chat_format_example(tmpls.get(), /* use_jinja= */ true, {});
+ assert_contains(example, "English (en-GB) to English (en-GB) translator");
+ assert_contains(example, "How are you?");
+
+ common_chat_templates_inputs inputs;
+ inputs.messages = { message_user };
+ inputs.add_generation_prompt = true;
+ inputs.chat_template_kwargs["source_lang_code"] = R"("en")";
+ inputs.chat_template_kwargs["target_lang_code"] = R"("fr")";
+
+ auto params = common_chat_templates_apply(tmpls.get(), inputs);
+ assert_contains(params.prompt, "English (en) to French (fr) translator");
+ assert_contains(params.prompt, "into French:\n\n\nHey there!<end_of_turn>\n");
+ assert_equals(std::string("<start_of_turn>model\n"), params.generation_prompt);
+ assert_ends_with(params.prompt, params.generation_prompt);
+
+ // typed text parts are joined into one item
+ inputs.messages = { message_user_parts };
+ params = common_chat_templates_apply(tmpls.get(), inputs);
+ assert_contains(params.prompt, "into French:\n\n\nHey\nthere<end_of_turn>\n");
+
+ // assistant prefill is appended after the generation prompt
+ inputs.messages = { message_user, message_assist_prefill_content };
+ inputs.add_generation_prompt = false;
+ inputs.continue_final_message = COMMON_CHAT_CONTINUATION_CONTENT;
+ params = common_chat_templates_apply(tmpls.get(), inputs);
+ assert_equals(std::string("<start_of_turn>model\nHello, "), params.generation_prompt);
+ assert_ends_with(params.prompt, "Hey there!<end_of_turn>\n<start_of_turn>model\nHello, ");
+}
+
// Verify reasoning-trace retention rules in the DeepSeek-V4 template:
// all traces are retained unless drop_thinking is true AND the conversation
// has no tool calls, in which case only the last (after-final-user) trace is
@@ -7986,6 +8030,7 @@ int main(int argc, char ** argv) {
test_tools_oaicompat_json_conversion();
test_convert_responses_to_chatcmpl();
test_developer_role_to_system_workaround();
+ test_translate_gemma();
test_deepseek_v4_thinking_retention();
test_deepseek_v4_tool_result_ordering();
test_template_generation_prompt();