Skip to content

[RFC] autoparser unable to accommodate non-ASCII reasoning markers, e.g. ◁think▷#25369

Open
ulidtko wants to merge 1 commit into
ggml-org:masterfrom
ulidtko:master
Open

[RFC] autoparser unable to accommodate non-ASCII reasoning markers, e.g. ◁think▷#25369
ulidtko wants to merge 1 commit into
ggml-org:masterfrom
ulidtko:master

Conversation

@ulidtko

@ulidtko ulidtko commented Jul 6, 2026

Copy link
Copy Markdown

Hi @pwilkin and team!

In follow-up to #18675, let me open an issue after repeatedly failing to find prior discussion.

The attached patch is mostly to demonstrate an issue — it's short enough to tell exactly what is being "fixed". Below I'll describe textually anyway. I'm not convinced it's the best possible approach; sure enough, you'll find a better idea. I'm perfectly fine if the PR gets closed, if anyone more familiar finds a better way.

So, the issue reproduces when running this model: ggml-org/Kimi-VL-A3B-Thinking-2506-GGUF,

The surface-level symptom is, llama.cpp not parsing the reasoning stream properly:

image

Do notice the uncommon ◁think▷ marker; the closing one ◁/think▷ follows below in response. The characters are:

  • U+25C1 WHITE LEFT-POINTING TRIANGLE
  • U+25B7 WHITE RIGHT-POINTING TRIANGLE

These are a property of how the Thinking fine-tune was conducted, I believe. The multi-token sequences ◁think▷, ◁/think▷ are documented in the models readme's, too.

... With this patch, and a correction in chat template jinja, I'm getting reasoning_mode: TAG_BASED and proper parses in both llama-cli and llama-server:

image

However again, I'm clear that this is only a rough first cut, a PoC draft that worked; the added logic is too specific and not a good proper fix.

It is necessary, because segmentize_markers() cannot handle anything besides <foo> and [bar]:

auto is_marker_opener = [](char c) -> bool { return c == '<' || c == '['; };
auto is_marker_closer = [](char op, char c) -> bool { return (op == '<' && c == '>') || (op == '[' && c == ']'); };

(Plus a related change in PEG builder. This patch is made and tested against version b9747, but the same code lives in today's master ☝️)

The real question is:

  • Can the autoparser be generalized to deduce this weirdness, too, from tokenizer.chat_template alone?
  • I.e. without requiring code change / nasty hacks like this one.

Hence the RFC tag — Request For Comment.


Fixed & original chat templates
Original, distributed with model
{%- for message in messages -%}
  {%- if loop.first and messages[0]['role'] != 'system' -%}
    {{'<|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|>'}}
  {%- endif -%}
  {%- if message['role'] == 'system' -%}{{'<|im_system|>'}}{%- endif -%}
  {%- if message['role'] == 'user' -%}{{'<|im_user|>'}}{%- endif -%}
  {%- if message['role'] == 'assistant' -%}{{'<|im_assistant|>'}}{%- endif -%}
  {{- message['role'] -}}{{'<|im_middle|>'}}

  {%- if message['content'] is string -%}
    {{- message['content'] + '<|im_end|>' -}}
  {%- else -%}
    {%- for content in message['content'] -%}
      {%- if content['type'] == 'image' or 'image' in content or 'image_url' in content -%}
        {{'<|media_start|>image<|media_content|><|media_pad|><|media_end|>'}}
      {%- else -%}
        {{content['text']}}
      {%- endif -%}
    {%- endfor -%}
    {{'<|im_end|>'}}
  {%- endif -%}
{%- endfor -%}
{%- if add_generation_prompt -%}{{'<|im_assistant|>assistant<|im_middle|>'}}{%- endif -%}
Fixed, works as expected together with the PR's patch
{%- for message in messages -%}
  {%- if loop.first and messages[0]['role'] != 'system' -%}
    <|im_system|>system<|im_middle|>You are a helpful AI assistant called Kimi Dolores. You live in the house of Wizard Max. You have a gift of vision, use it wisely.<|im_end|>
  {%- endif -%}
  {%- if message['role'] == 'system' -%} {{'<|im_system|>'}} {%- endif -%}
  {%- if message['role'] == 'user' -%} {{'<|im_user|>'}} {%- endif -%}
  {%- if message['role'] == 'assistant' -%} {{'<|im_assistant|>'}} {%- endif -%}
  {{- message['role'] -}}{{'<|im_middle|>'}}

  {%- if message.get('reasoning_content') -%}
    ◁think▷{{ message['reasoning_content'] }}◁/think▷
  {%- endif -%}

  {%- if message['content'] is string -%}
    {{- message['content'] + '<|im_end|>' -}}
  {%- else -%}
    {%- for content in message['content'] -%}
      {%- if content['type'] == 'image' or 'image' in content or 'image_url' in content -%}
        {{'<|media_start|>image<|media_content|><|media_pad|><|media_end|>'}}
      {%- else -%}
        {{content['text']}}
      {%- endif -%}
    {%- endfor -%}
    {{'<|im_end|>'}}
  {%- endif -%}
{%- endfor -%}
{%- if add_generation_prompt -%}
  {{'<|im_assistant|>assistant<|im_middle|>'}}
{%- endif -%}

AI usage disclosure: Anthropic Sonnet 4.6 wrote this garbage of patch, with iterated verbal abuse direction provided by the operator (me). But hey, at least it was able to read the docs & follow the source — well done, good codebase. This PR description is 100% pure human writing; best regards.

Models such as Kimi-VL-A3B-Thinking-2506 emit CoT reasoning wrapped in
◁think▷...◁/think▷ (U+25C1 WHITE LEFT-POINTING TRIANGLE + "think" +
U+25B7 WHITE RIGHT-POINTING TRIANGLE).  These are multi-byte UTF-8
sequences, not ASCII angle-brackets, so both segmentize_markers() and
p.marker() silently classified them as plain TEXT, causing the autoparser
to infer reasoning_mode=NONE even when the chat template correctly uses
reasoning_content around those delimiters.

Fix:
- segmentize_markers(): add two new else-if branches that detect ◁ as a
  multi-byte opener and ▷ as the corresponding closer, producing MARKER
  segments identical in role to <...> and [...] ones.  The for-loop step
  is preserved unchanged; the branches adjust cur_pos with ±(size-1) to
  account for the 3-byte sequences inside the single-increment loop.
- p.marker(): add a third choice branch that matches ◁...▷ spans, keeping
  it symmetric with the existing sharp- and square-bracket branches.

After this change, llama-debug-template-parser should report
reasoning_mode: TAG_BASED with the correct start/end tags for
Kimi-VL-Thinking templates.

Co-Authored-By: Claude Code <claude-code@anthropic.com>
@ulidtko ulidtko requested a review from a team as a code owner July 6, 2026 20:16
@ggml-gh-bot

ggml-gh-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

Hi @ulidtko, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • AI-generated content: This project does not accept PRs, descriptions or commit messages that are fully or predominantly AI-generated. If you have used AI to assist you in writing code, please make sure to disclose that explicitly.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant