From bbd6b53174ece541ac365b96886fceb05a32cf4a Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Sat, 26 Sep 2026 13:04:07 -0300 Subject: [PATCH] convert: Zamba2-VL template, images as llama.cpp's media markers llama-server passes a message with media to the template as a string holding a marker, <__media___> with an id random per server, where each image was. The template only knew the literal <__media__>, so the marker stayed in the user text: the image landed inside the user turn instead of in front of it, where Zyphra's template puts it. Find markers by their <__media_ prefix, put them in front of the user turn, and keep Zyphra's list form as it was. Checked: rendered with jinja2, the template gives Zyphra's prompt for its list form and for string content with <__media__> or a random marker; in llama-server (Zamba2-VL-2.7B, --chat-template-file) the image now starts at prompt index 2 (, <|vision_start|>), as in Zyphra's processor, where it started at 4. Co-Authored-By: Claude Opus 5.5 --- conversion/zamba2vl.py | 48 ++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/conversion/zamba2vl.py b/conversion/zamba2vl.py index 825f4b8510e2..bbaefc9f5f59 100644 --- a/conversion/zamba2vl.py +++ b/conversion/zamba2vl.py @@ -67,27 +67,39 @@ def set_vocab(self): special_vocab.chat_template = self._CHAT_TEMPLATE special_vocab.add_to_gguf(self.gguf_writer) - # Zyphra's template only takes content as a list of parts. llama.cpp passes a string, with a - # media marker where each image was; this one takes both, and puts the images in front of the - # user turn as Zyphra's does (mtmd wraps each in <|vision_start|> ... <|vision_end|>). + # Zyphra's template only takes content as a list of image and text parts. llama.cpp passes a + # message with media as a string holding a marker, <__media___> with an id random per server, + # where each image was; this one takes both, and puts the images (the markers) in front of the + # user turn, as Zyphra's does (mtmd wraps each image in <|vision_start|> ... <|vision_end|>). _CHAT_TEMPLATE = ( - "{%- for message in messages -%}" + '{%- for message in messages -%}' "{%- if message['content'] is string -%}" - "{%- set text = message['content'] -%}" - "{%- set n_img = (text.split('<__media__>') | length) - 1 -%}" - "{%- set text = text.replace('<__media__>', '') | trim -%}" - "{%- else -%}" - "{%- set text = message['content'] | selectattr('type', 'equalto', 'text') | map(attribute='text') | join('') -%}" - "{%- set n_img = message['content'] | selectattr('type', 'equalto', 'image') | list | length -%}" - "{%- endif -%}" + "{%- set ns = namespace(text=message['content'], media='') -%}" + '{%- else -%}' + "{%- set ns = namespace(text=message['content'] | selectattr('type', 'equalto', 'text') | map(attribute='text') | join(''), media='') -%}" + "{%- for c in message['content'] | selectattr('type', 'equalto', 'image') -%}" + "{%- set ns.media = ns.media ~ '<|vision_start|><|vision_end|>\\n' -%}" + '{%- endfor -%}' + '{%- endif -%}' + "{%- set segs = ns.text.split('<__media_') -%}" + '{%- if segs | length > 1 -%}' + '{%- set ns.text = segs[0] -%}' + '{%- for seg in segs[1:] -%}' + "{%- set bits = seg.split('>') -%}" + "{%- set ns.media = ns.media ~ '<__media_' ~ bits[0] ~ '>\\n' -%}" + "{%- set ns.text = ns.text ~ (bits[1:] | join('>')) -%}" + '{%- endfor -%}' + '{%- set ns.text = ns.text | trim -%}' + '{%- endif -%}' "{%- if message['role'] == 'user' -%}" - "{%- for i in range(n_img) -%}{{ '<__media__>\\n' }}{%- endfor -%}" - "{{ 'user\\n' ~ text ~ '\\n' }}" - "{%- else -%}" - "{{ '' ~ message['role'] ~ '\\n' ~ text ~ '' }}" - "{%- endif -%}" - "{%- endfor -%}" - "{%- if add_generation_prompt -%}{{ 'assistant\\n' }}{%- endif -%}" + "{{ ns.media ~ 'user\\n' ~ ns.text ~ '\\n' }}" + '{%- else -%}' + "{{ '' ~ message['role'] ~ '\\n' ~ ns.text ~ '' }}" + '{%- endif -%}' + '{%- endfor -%}' + '{%- if add_generation_prompt -%}' + "{{ 'assistant\\n' }}" + '{%- endif -%}' )