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_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("") != std::string::npos && src.find("(); + } 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 = "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 -%} + {{ '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] -%} + {{ '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' + }} + {%- 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 -%} + {{ '\n' }} +{%- endfor -%} +{%- if add_generation_prompt -%} + {{'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 ... { 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!\n"); + assert_equals(std::string("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\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("model\nHello, "), params.generation_prompt); + assert_ends_with(params.prompt, "Hey there!\nmodel\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();