Repository navigation
String/typed content caps differ from llama.cpp for SmolVLM, TranslateGemma 2B, Ministral 3 and LFM2.5 #720
Description
TemplateCaps.detect reports string/typed content support that differs from llama.cpp's /props chat_template_caps for five templates. llamadart uses these caps to decide how to shape message content: whether to take the typed-content path when media is present. Because the caps are wrong, prompts differ from llama-server.
| Template | llama.cpp 7fe450e19 (string / typed) |
llamadart at ad8dd330d |
|---|---|---|
SmolVLM-500M-Instruct: the template embedded in SmolVLM-500M-Instruct-Q8_0.gguf from ggml-org/SmolVLM-500M-Instruct-GGUF, sha256 9d4612de… |
false / true | true / true |
TranslateGemma 2B: test/fixtures/templates/TranslateGemma-2B-it.jinja |
false / true | true / true |
Ministral 3 3B Reasoning: test/fixtures/templates/Ministral-3-3B-Reasoning.jinja |
true / false | true / true |
Ministral-3-14B-Reasoning-2512: llama.cpp models/templates |
true / false | true / true |
LFM2.5-8B-A1B: llama.cpp models/templates |
true / false | true / true |
The other 92 templates in the #719 differential that llama-server loads agree.
How the repros were run
- Each repro renders with
ChatTemplateEngine.render(templateSource: <template>, messages: …, metadata: {bos '<s>', eos '</s>'})atad8dd330d. - The llama-server reference is
7fe450e19POST /apply-templatewith the same conversation, run as--jinja --chat-template-file <template>:- text conversations on
stories15M.gguf; - image conversations on
LFM2-VL-450M-Q4_0.ggufwith--mmproj mmproj-LFM2-VL-450M-Q8_0.gguf.
- text conversations on
- In the server output, the media marker is normalized to
<__media__>, and the model's<|im_end|>EOS is compared as</s>.
SmolVLM: text-only chats lose all message text
Conversation, no tools: user Weather in Paris?; assistant Checking. plus a get_weather call; tool result {"temp":21.5,"sky":"clear","note":"it's"}.
llamadart (same at base eeb382b55):
<|im_start|>User: <end_of_utterance>
Assistant: <end_of_utterance>
Tool: <end_of_utterance>
Assistant:
llama-server:
<|im_start|>User: Weather in Paris?<end_of_utterance>
Assistant: Checking.<end_of_utterance>
Tool: {"temp":21.5,"sky":"clear","note":"it's"}<end_of_utterance>
Assistant:
- The template iterates
message['content']as parts. - llama.cpp converts every string to one text part for typed-only templates (
messages_inp_normalizer,common/chat.cpp:474). - llamadart passes strings, and uses the typed path only when a message has media (
chat_template_engine.dart:254). - So the fix needs both corrected caps and routing typed-only templates to parts even without media. I tried that locally: the same render becomes identical to llama-server.
Ministral 3: image plus a reasoning-only turn throws or drops the reasoning
Conversation: user image + Describe, assistant with reasoning Let me look. only, user More?.
- Ministral-3-14B throws
Assistant message must have a string or a list of chunks in content or a list of tool calls.llama-server renders…[INST]<__media_…__>Describe[/INST][THINK]Let me look.[/THINK]<|im_end|>[INST]More?[/INST](<|im_end|>is LFM2-VL's EOS). - Ministral 3 3B renders
…[INST]<__media__>Describe[/INST]</s>[INST]More?[/INST], without the[THINK]block. - Cause: because llamadart thinks the template takes typed content, the image path goes through
renderWithMultimodalContent, which skipsMinistralHandler's block serialization. - With llama.cpp's caps (string-only) forced locally, all 12 image renders of each Ministral template in the differential are identical to llama-server. Today it is 2 of 12, plus 4 throws for 14B.
LFM2.5-8B-A1B: correcting the caps alone makes it worse
- With an image, llamadart's typed path currently happens to match llama-server: 8 of 12 image renders are identical.
- With llama.cpp's caps (string-only) forced, only 4 of 12 match.
- The string path goes through
Lfm2Handler, which flattens the tool list:List of tools: [{"name": "get_weather", …instead of[{"type": "function", "function": {"name": "get_weather", …. - This is part 2 of #716. Fix it together with this issue.
TranslateGemma 2B: correcting the caps alone makes it worse
- With llama.cpp's caps (typed-only) forced, the empty assistant turn gains
[{'type': 'text', 'text': ''}]in the prompt. llama-server prints nothing there. TranslateGemmaHandler's content normalization needs to change with the caps.
Related: Kimi-K2 tool results on the image path (separate cause, blocked by this issue)
- With an image in the conversation,
moonshotai-Kimi-K2.jinjarenders the tool result as## Return of call_0\n[{'type': 'text', 'text': 'sunny'}]. llama-server renders## Return of call_0\nsunny. - Kimi-K2's caps match llama.cpp (both true). The cause is that llamadart's typed path turns every message's content, including tool results, into parts. llama.cpp keeps strings unless the template is typed-only.
- Making tool results strings on the typed path fixes Kimi-K2, but moved 8 SmolVLM image renders away from llama-server, because of SmolVLM's wrong caps. So it has to follow this fix.
All of this exists at base eeb382b55, before #719. It was found in #719's multimodal differential.
Expected
TemplateCapsstring/typed content caps match llama.cpp/propsfor these templates, pinned intemplate_caps_cache_fixtures_test.dart.- Typed-only templates get text parts for every message, as llama.cpp's normalizer does.
- The Ministral, LFM2.5 and TranslateGemma handlers produce llama-server's prompts under the corrected caps. Pin each with a multimodal
/apply-templatefixture intest/fixtures/template_message_json_upstream.json. - Then give tool results on the typed path as strings, unless the template is typed-only (Kimi-K2).
Activity