Packages
llama_cpp_ex
0.8.35
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.28
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.0
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.3.0
0.2.0
Elixir bindings for llama.cpp — run LLMs locally with Metal, CUDA, Vulkan, or CPU acceleration.
Current section
Files
Jump to
Current section
Files
lib/llama_cpp_ex/thinking.ex
defmodule LlamaCppEx.Thinking do
@moduledoc """
Parser for `<think>...</think>` blocks in thinking model output.
Thinking models (e.g. Qwen 3.5 with `enable_thinking: true`) wrap their
chain-of-thought reasoning in `<think>...</think>` tags. This module provides
both a one-shot parser for complete text and a streaming parser that handles
token boundary splits.
"""
@think_open "<think>"
@think_close "</think>"
@think_open_len byte_size(@think_open)
@think_close_len byte_size(@think_close)
@doc """
Splits completed text into `{reasoning_content, content}`.
Handles both explicit `<think>...</think>` wrapping and the common case where
the chat template already opened the `<think>` block (so generated text starts
directly with reasoning followed by `</think>`).
## Examples
iex> LlamaCppEx.Thinking.parse("<think>I need to think</think>The answer is 42")
{"I need to think", "The answer is 42"}
iex> LlamaCppEx.Thinking.parse("reasoning here\\n</think>\\nThe answer is 42")
{"reasoning here", "The answer is 42"}
iex> LlamaCppEx.Thinking.parse("Just a response")
{"", "Just a response"}
iex> LlamaCppEx.Thinking.parse("<think>reasoning only</think>")
{"reasoning only", ""}
"""
@spec parse(String.t()) :: {String.t(), String.t()}
def parse(text) when is_binary(text) do
case text do
<<@think_open, rest::binary>> ->
split_at_close(rest)
_ ->
# Template may have already opened <think>, so generated text starts
# with reasoning directly followed by </think>
case :binary.match(text, @think_close) do
{pos, @think_close_len} ->
reasoning = binary_part(text, 0, pos)
content =
binary_part(
text,
pos + @think_close_len,
byte_size(text) - pos - @think_close_len
)
{String.trim_trailing(reasoning), String.trim_leading(content)}
:nomatch ->
{"", text}
end
end
end
defp split_at_close(rest) do
case :binary.match(rest, @think_close) do
{pos, @think_close_len} ->
reasoning = binary_part(rest, 0, pos)
content =
binary_part(rest, pos + @think_close_len, byte_size(rest) - pos - @think_close_len)
{String.trim_trailing(reasoning), String.trim_leading(content)}
:nomatch ->
# Unclosed think tag — treat entire content as reasoning
{rest, ""}
end
end
@doc """
Creates a new streaming parser state.
Use with `feed/2` to incrementally parse streamed tokens.
## Options
* `:thinking` - When `true`, assumes the template already opened a
`<think>` block, so generated text starts in thinking mode. Defaults
to `false`.
"""
@spec stream_parser(keyword()) :: map()
def stream_parser(opts \\ []) do
if Keyword.get(opts, :thinking, false) do
%{state: :thinking, buffer: ""}
else
%{state: :init, buffer: ""}
end
end
@doc """
Feeds a text chunk to the streaming parser.
Returns `{events, new_parser}` where events are `{:thinking, text}` or
`{:content, text}` tuples.
The parser buffers partial `<think>` and `</think>` tags to correctly handle
token boundary splits.
## Examples
parser = LlamaCppEx.Thinking.stream_parser()
{events, parser} = LlamaCppEx.Thinking.feed(parser, "<think>")
# events = [] (tag consumed)
{events, parser} = LlamaCppEx.Thinking.feed(parser, "reasoning")
# events = [{:thinking, "reasoning"}]
{events, _parser} = LlamaCppEx.Thinking.feed(parser, "</think>answer")
# events = [{:content, "answer"}]
"""
@spec feed(map(), String.t()) :: {[{:thinking | :content, String.t()}], map()}
def feed(%{state: state, buffer: buffer} = parser, text) when is_binary(text) do
input = buffer <> text
do_feed(state, input, parser)
end
# --- :init state ---
# Looking for <think> at the start. Buffer partial prefixes.
defp do_feed(:init, input, parser) do
cond do
# Full <think> tag found at start
String.starts_with?(input, @think_open) ->
rest = binary_part(input, @think_open_len, byte_size(input) - @think_open_len)
# Transition to :thinking, process rest
do_feed(:thinking, rest, %{parser | state: :thinking, buffer: ""})
# Input could be a prefix of <think> — keep buffering
String.starts_with?(@think_open, input) and byte_size(input) < @think_open_len ->
{[], %{parser | buffer: input}}
# Not a think tag — everything is content
true ->
events = if input != "", do: [{:content, input}], else: []
{events, %{parser | state: :content, buffer: ""}}
end
end
# --- :thinking state ---
# Emit {:thinking, ...} events. Watch for </think>.
defp do_feed(:thinking, input, parser) do
case :binary.match(input, @think_close) do
{pos, @think_close_len} ->
# Found closing tag
thinking_text = binary_part(input, 0, pos)
rest =
binary_part(input, pos + @think_close_len, byte_size(input) - pos - @think_close_len)
events = if thinking_text != "", do: [{:thinking, thinking_text}], else: []
rest_events = if rest != "", do: [{:content, rest}], else: []
{events ++ rest_events, %{parser | state: :content, buffer: ""}}
:nomatch ->
# Check if input ends with a partial </think> prefix
{safe, maybe_tag} = split_trailing_tag_prefix(input, @think_close)
events = if safe != "", do: [{:thinking, safe}], else: []
{events, %{parser | buffer: maybe_tag}}
end
end
# --- :content state ---
# Everything is content, no more tag parsing needed.
defp do_feed(:content, input, parser) do
events = if input != "", do: [{:content, input}], else: []
{events, %{parser | state: :content, buffer: ""}}
end
# Splits input into {safe_to_emit, possible_tag_prefix} where the suffix
# could be the beginning of the given tag.
defp split_trailing_tag_prefix(input, tag) do
tag_len = byte_size(tag)
input_len = byte_size(input)
# Check suffixes of length 1..min(tag_len-1, input_len)
max_check = min(tag_len - 1, input_len)
result =
Enum.find(max_check..1//-1, fn len ->
suffix = binary_part(input, input_len - len, len)
prefix = binary_part(tag, 0, len)
suffix == prefix
end)
case result do
nil ->
{input, ""}
len ->
safe = binary_part(input, 0, input_len - len)
tail = binary_part(input, input_len - len, len)
{safe, tail}
end
end
end