Skip to content

String/typed content caps differ from llama.cpp for SmolVLM, TranslateGemma 2B, Ministral 3 and LFM2.5 #720

Closed

Description

@leehack

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>'}) at ad8dd330d.
  • The llama-server reference is 7fe450e19 POST /apply-template with the same conversation, run as --jinja --chat-template-file <template>:
    • text conversations on stories15M.gguf;
    • image conversations on LFM2-VL-450M-Q4_0.gguf with --mmproj mmproj-LFM2-VL-450M-Q8_0.gguf.
  • 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 skips MinistralHandler'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.jinja renders 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

  • TemplateCaps string/typed content caps match llama.cpp /props for these templates, pinned in template_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-template fixture in test/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

  1. added
    bugSomething isn't working
    structured-outputJSON schema, grammar, and constrained output work
    priority:P2Planned next: useful unblocked work or validation after P1 items
    on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:P2Planned next: useful unblocked work or validation after P1 itemsstructured-outputJSON schema, grammar, and constrained output work

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions