Releases: benoitc/erllama
Releases · benoitc/erllama
Release list
erllama 0.11.0
Changed
chat/3defaultsreasoning_formattodeepseek: thinking models
now return their thinking text inreasoning_contentinstead of
inline incontent(llama-server's default). Opt out per call with
reasoning_format => none.chat_apply/3returns the template's constraint set alongside
promptandparams:sampler_opts,stop_sequences,
generation_prompt,supports_thinking,thinking_start_tag,
thinking_end_tags. Mergesampler_opts+stop_sequencesinto
thestream/3options (the tool-calls guide shows how).
Added
erllama:fork_session/3: duplicate a sticky session's live KV into
a new session (llama_memory_seq_cp), so two continuations explore
different branches without re-prefilling the shared prefix. Works
on every model family; never queues (seq_capacitywhen no
sequence is free).- Load progress:
progress_to => Pidonload_modeldelivers
{erllama_load_progress, ModelId, Float}messages (whole-percent
throttled, final1.0) while the GGUF loads. - Native logs: llama.cpp / ggml log lines are forwarded into
logger
under the domain[erllama, native], gated by thenative_log_level
application env (none | error | warning | info | debug, default
warning). - Full llama.cpp sampler surface as request options:
typical_p,
top_n_sigma,xtc_probability/xtc_threshold,
dynatemp_range/dynatemp_exponent,min_keep,
frequency_penalty/presence_penalty/penalty_last_n, DRY
(dry_multiplier,dry_base,dry_allowed_length,
dry_penalty_last_n,dry_sequence_breakers),mirostat(1 | 2)
withmirostat_tau/mirostat_eta,logit_bias,ignore_eos
andinfill. Chain order follows llama.cpp's default; see the
sampling reference in the configuration guide. - Per-token logprobs:
logprobs => N(1..32) emits
{erllama, Ref, {logprobs, #{token_id, logprob, top}}}stream
events and adds alogprobslist tocomplete/3/collect/2
results. Full-vocab log-softmax over the raw model distribution
(OpenAI semantics). erllama:vocab_info/1: vocabulary size,add_bos/add_eos, and
the special / FIM token ids (bos,eos,eot,sep,nl,
pad,mask,fim_pre,fim_suf,fim_mid,fim_pad,
fim_rep,fim_sep) for fill-in-the-middle prompt assembly.erllama:detokenize/3withremove_special/unparse_special
(renders special tokens), backed byllama_detokenize.
detokenize/2is untouched: the cache byte-keys are computed over
its output.- Tool-call grammar enforcement:
chat/3merges the grammar llama.cpp
synthesizes from the chat template into the request, so
tool_choice => requiredalways yields a parsed call and
tool_choice => autoarms a lazy grammar that constrains the reply
once the model opens a call. Template-declared stop strings are
honoured. A callergrammarcombined with active tools is rejected. json_schemachat option (OpenAIresponse_formatsemantics): the
reply's content is grammar-constrained to the schema. Rejected in
combination withtools.- Chat options
enable_thinking(default true) and
continue_final_message(none | auto | content | reasoning) for
assistant prefill. - Request options
grammar_lazy,trigger_patterns,
trigger_tokens,grammar_prefill: the lazy / template-grammar
sampler shape (llama_sampler_init_grammar_lazy_patterns), usable
directly withstream/3and friends.
Fixed
- Cache correctness on recurrent / hybrid models (Mamba, RWKV, Jamba,
Qwen3-Next, LFM2, ...): a warm exact hit no longer duplicates the
last prompt token when the memory refuses the primer's partial
removal; the engine falls back to a cold prefill and bumps the new
restore_failedcounter. A failedkv_unpackno longer crashes the
model process (the row is treated as a miss). - Partial warm hits (extended prompts) prefill the suffix directly
instead of dropping and re-decoding the last cached token: one fewer
decoded token per partial hit, and no partialseq_rm, so the path
stays warm on recurrent models. nif_kv_seq_rmrefreshes the per-seq position bookkeeping on
failure too, keeping Erlang and llama.cpp in agreement when a
partial removal is refused.
Added
- Model-family probe at load:
model_info/1reportsarch,
n_ctx_train,n_params,n_embd,n_layer,n_swa,
recurrentandhybrid(llama backend). Encoder-decoder (T5) and
diffusion archs are rejected at load with
{error, {unsupported_model, encoder_decoder | diffusion}}instead
of failing confusingly at the first decode. - Context option
n_rs_seq(recurrent-state rollback snapshots per
seq); defaults to 1 on recurrent / hybrid models so warm hits work
where the arch supports rollback. split_mode => tensor(experimental upstream tensor-parallel
split;rowis deprecated upstream).- Cache counter
restore_failed; stub backend knobs
fail_seq_rm_last/fail_kv_unpackto test the fallback paths.
erllama 0.10.1
Fixed
chat/3andchat_apply/3hand tools to llama.cpp in the OpenAI
shape ({"type": "function", "function": {...}}); the flat
chat_tool()maps were rejected withchat_parse_failed.erllama_chat_SUITEstarts the application with its dependencies when
run on its own.
Added
- Load option
chat_template: Jinja source replacing the template
stored in the GGUF forchat/3/chat_apply/3. examples/agent_loop: a tool-calling agent loop onchat/3.
erllama 0.10.0
Changed
- Vendored llama.cpp bumped from b10068 to b10593 (
vendor/is now kept
whole: upstream builds it as CMake targets).model_optsgains
load_mode(upstream'sllama_load_mode);use_mmap/use_mlock
are mapped onto it.
Removed (BREAKING)
unload_model/1(useunload/1),models/0(use
list_models/0),list_cached_prefixes/2(renamed
cached_prefix_len/2).infer/4: usestream/3(text or tokens; the receiving
process is thetooption, default the caller).continue/3takes
toinstead ofcaller_pid; a missingsession_idis
{error, {missing_option, session_id}}.- Stream messages
{erllama_token, Ref, _},{erllama_token_id, Ref, _},{erllama_thinking_end, Ref, _},{erllama_done, Ref, _},
{erllama_error, Ref, _}: every event is now{erllama, Ref, Event}withEvent :: {token, Bin} | {token_id, Id} | {thinking, Bin} | {thinking_end, Sig} | {done, Stats} | {error, Reason}
(erllama:stream_event()). apply_chat_template/2renamedrender_chat_template/2.chat_apply/2ischat_apply/3(model, messages, opts) and
returns{ok, #{prompt, params}}; messages and tools are Erlang
maps, JSON encoding happens at the NIF boundary.verify/4returns{ok, #{accepted, next}}.set_observer/1andclear_observer/0: use a
middleware (erllama_middleware,guides/middleware.md).- Application environment:
chat_params_cache_sizerenamed
chat_cache_size;quota_mbdropped from thetiersentries.
Changed (BREAKING)
- Every per-model call returns
{ok, Result}or{error, not_loaded}
for an unknown or stopped model instead of exiting withnoproc:
model_info/1,status/1,phase/1,pending_len/1,
queue_depth/1,last_cache_hit/1,list_adapters/1now wrap
their result in{ok, _};unload/1,evict/1,shutdown/1,
end_session/2return{error, not_loaded}. load_model/1,2validates the config (erllama_opts):backend
defaults toerllama_model_llama; a missingmodel_pathis
{error, {missing_config, model_path}}, a missing file is
{error, {invalid_config, model_path, Path}}, an unknown key is
{error, {unknown_option, Key}}.model_idin the config map is
honoured byload_model/1.complete/3,prefill_only/3andinfer/4validate their option
maps: unknown keys are{error, {unknown_option, Key}}, wrong types
{error, {invalid_option, Key, Value}}.response_tokensdefaults to 64 on every path (complete/3used 4).evict/1andshutdown/1honour theevict_save_timeout_ms
application environment key (default 30 s); it was documented but
unread.list_adapters/1entries use the keyadapter(washandle).
Added
erllama:stream/3anderllama:collect/2: streaming inference
with a typed event envelope and a collector that folds the events
into astream_result().erllama:chat/3: one chat turn (render, generate, parse) with
Erlang-term messages and tools; returns the parsed assistant
message with content, reasoning and tool calls.erllama:embed/2accepts text;erllama:embed_batch/2embeds a
list of inputs in one round-trip to the model process.erllama:whereis/1returns the model pid for monitoring.- Supervised cache tiers:
erllama_cache:add_tier/1,remove_tier/1,
list_tiers/0,info/0, and thetiersapplication environment
key ([#{name, backend => disk | ram_file, root}]) started with
the application.load_modelchecks thattier_srvis running and
matchestier. erllama_middleware: hackney-style middleware chain around every
API call (global via themiddlewareenvironment key, or per call
with themiddlewareoption).erllama:pressure/0,pressure_sources/0,requests/0,
request_info/1.- Application environment keys
fingerprint_mode(now the default
for models that do not set it),writer_max_concurrent,
chat_cache_size,thinking_signing_key,middlewareandtiers
are declared in the app file and documented;os_monis a
declared dependency (thesystempressure source needs memsup). erllama_scheduler:validate_config/1checks thatmodel_evictor
names a loadable module exportingevict_one/0.- Documentation: public modules are
erllama,erllama_cache,
erllama_middleware,erllama_schedulerand the
erllama_model_backend,erllama_model_evictor,erllama_pressure
behaviours plus theerllama_model_stubtest backend; every other
module is hidden from hexdocs. Guides rewritten around the public
API (the tool-calls guide now documentschat/3; the
tool_call_markersoption it described never existed). Public
types are defined inerllama. - Tests: shared
erllama_test_helpers; nocatch Exprleft, so the
suite compiles on OTP 29 withoutnowarn_deprecated_catch. erllamaexports the types its specs use (token_id/0,
cache_key/0,completion_result/0,stats/0,request_opts/0,
load_config/0,error_reason/0, ...) and documents every error
reason inerror_reason/0.
erllama 0.9.0
Added
erllama_chat:set_observer/1' /clear_observer/0' hook.
Lets a separate module (typically the server's metrics module)
observe wall-time of every chat-NIF call (apply / parse) without the runtime taking a compile-time
dependency on the metrics module. Registered via persistent_term;
unset = no-op. Drives the new
`erllama_chat_*_duration_seconds' Prometheus histograms
on the server side.erllama_app:start/2' enableserlang:system_flag(scheduler_wall_time, true)' at boot so callers
(the server's metrics module among them) can read per-scheduler
busy ratios for the autoparser dirty-pool monitoring.
Changed
chat_apply/2' runs upstream'scommon_chat_templates_apply' once per
request (prompt + parser). The per-tools params cache and the
render-only NIF are gone; only the per-model templates ref is
cached.- Vendored llama.cpp is b10068, upstream and unmodified (no local
patches).scripts/vendor_llama.sh <tag>performs bumps and fails
if any vendored file differs from the tarball. Unusedvendor/
libraries are pruned andLLAMA_OPENSSLis off (no OpenSSL link). - Hex package manifest now includes
c_src/erllama_chat_nif.{cpp,h}
andc_src/erllama_resources.h(previous tarballs could not build). - Bump vendored llama.cpp from b9334 to b9585. No API-breaking changes
on our touchpoints; bringscommon/chat*bug fixes (LFM2 reasoning,
tool-parser unification). UPDATE_LLAMA.md refreshed to document
thecommon/+vendor/sync the prior procedure left implicit.
Removed (BREAKING)
- Streaming API:
{erllama_token, _, {tool_call_delta, _}}' anderllama_tool_call_end' messages, plus the matching
step result variants. The engine no longer classifies tool-call
bytes mid-stream. Callers usingerllama:infer/4' directly buffer tokens and callerllama:chat_parse/3'
at done for structured extraction. HTTP wire format on the
server is unchanged. - Backend `tool_call_end_is_eos/1' callback.
Changed
erllama:chat_apply/3' becomeschat_apply/2' (drops the
ToolsHash' parameter).erllama_chat_cache' shrinks to
caching only the heavycommon_chat_templates_init' ref per model; each request invokescommon_chat_templates_apply' fresh because
the synthesized parser is sensitive totool_choice' andparallel_tool_calls' (now folded into the NIF Inputs map).
Added
- Public
erllama:chat_apply/2+chat_parse/3that delegate
toerllama_chatvia the model gen_statem so callers can
build achat_params_ref' and parse model output without touching the underlying NIF model resource. Backend gains an optionalget_model_ref/1' callback; the stub backend returns
{error, chat_not_supported}'.chat_purge/1' drops cached entries
on demand for a given model id.
Fixed
- EOS-bounded tool-call flush: don't capture the EOS token's bytes
before flushing the span. The previous in-span EogFlag path called
req_tool_call_emit/3unconditionally; for Granite / Phi-4 the EOS
is a special token that detokenizes empty underspecial=false,
so the buffer stayed clean in practice, but the path was fragile
for any future model whose EOS detokenises to visible bytes. Now
the EOS-end branch skips the emit and callsreq_tool_call_end/2
directly, matching the byte-string-end-marker semantics.
Added
- Complete the chat-autoparser NIF wiring.
nif_chat_templates_apply
now translates an Erlang inputs map (messages+toolsJSON
binaries, optionaltool_choice) intocommon_chat_templates_inputs
viacommon_chat_msgs_parse_oaicompat/common_chat_tools_parse_oaicompat,
calls upstream, and returns the synthesizedchat_params_refplus
the rendered prompt bytes.nif_chat_parsedeserialises the
per-template PEG arena from the cached params string and dispatches
tocommon_chat_parse. The parsedcommon_chat_msgis marshalled
to#{role, content, reasoning_content, tool_calls}; tool-call
arguments come back as raw JSON binaries and are decoded to maps
at the Erlang facade boundary (erllama_chat:parse/3).
Newerllama_chat_SUITEreal-model CT (gated on
LLAMA_TEST_MODEL) covers init / apply / parse round-trip and a
partial-then-full streaming case. - NIF wrapper for llama.cpp's
common_chat_*autoparser. Vendors
common/+vendor/nlohmann+vendor/cpp-httplibfrom the
pinned llama.cpp tree, flipsLLAMA_BUILD_COMMON=ON, and links
llama-commoninto the NIF .so. Three new entry points
(nif_chat_templates_init/2,nif_chat_templates_apply/2,
nif_chat_parse/3) run on dirty CPU; two new resources
(chat_templates_ref,chat_params_ref) wrap
common_chat_templates_ptrandcommon_chat_params. New Erlang
facadeerllama_chat(raw NIF shim) and
erllama_chat_cache(LRU cache keyed on
{ModelIdBin, ToolsHash}, withpurge/1for model unload). The
refactorederllama_resources.hexposes the existing
C resource type pointers +erllama_model_tto C++
TUs behind anextern "C"guard.templates_initworks
end-to-end;templates_applyandparseship as
{error, not_implemented}placeholders (the Erlang-term marshalling
lands in the follow-up). Dormant capability: the chat / messages /
responses handlers do not consume this surface yet; Phase 3.C
wires them up. - EOS-bounded tool-call end-marker capture. Models configured with
tool_call_markers => #{start => Bytes, 'end' => <<"$eos">>}opt
into the new path: when the scheduler is inside an open tool-call
span and the model samples a token withEogFlag = 1, the
accumulatedtool_call_bytesbuffer is flushed via the existing
erllama_tool_call_endmessage before the request
finishes. Previously the buffer was silently dropped in that
branch (the only emission site was the byte-string end-marker
match inerllama_model_llama:map_marker/2). The
byte-string-end families (Mistral</s>, Qwen</tool_call>,
DeepSeek<|tool▁call▁end|>, Llama 3.1<|eom_id|>) are
byte-exact unchanged; the new path is opt-in via the sentinel
binary<<"$eos">>on theend' key. Backend behaviour gains one optionaltool_call_end_is_eos/1callback (the scheduler defaults tofalse` for backends that have not been updated).
Targets the IBM Granite-3.x and Microsoft Phi-4-mini families
whose wire shape bounds the call at EOS rather than a
byte-string end marker; the server-side migrations land in
follow-up PRs.
Changed
- The native tool-call capture splits spans on a repeated start marker. Families like
Mistral tekken delimit parallel calls by emitting the start marker again
([TOOL_CALLS]n1[ARGS]a1[TOOL_CALLS]n2[ARGS]a2</s>) with no per-call end between
them; the previous single-span capture concatenated every call into one
erllama_tool_call_endmessage, which the server parsed as one garbled
call.apply_step_results/2now finalises the current span before opening the next
when a start marker fires while already in-span, so each call yields its own end
message and reuses the upstream per-call accumulation. Behaviour-preserving for
families with explicit per-call end markers (qwen-xml, qwen3-coder, dsml): a start
never fires while in-span because the end fires first. The stub backend gains an
optionaltool_call_script :: [start | body | end_tok]config so tests can drive
exact decode-step sequences (the Mistral tekken[start, body, body, start, body, body, end_tok]shape and the qwen-style[start, body, end_tok, start, body, end_tok]regression guard run from the same harness). - The tool-call end marker now carries the source token's eog flag.
erllama_model_llama:map_marker/2was discardingEogFlagon the end
marker, so a model whose end marker IS the eos token (Mistral tekken uses</s>as
both the per-span end marker and the assistant turn's eos) kept decoding past the
close and spammed repeated tool calls under the greedy continuation. Backend step
results emit{tool_call_end, Eog}instead of the bare atom; the scheduler sets
Req.finishing = trueon eog so the turn ends cleanly. Backend type spec updated
accordingly. erllama_model_stubgains an opt-instep_delay_ms :: non_neg_integer()
test knob thattimer:sleeps for the configured number of milliseconds at the top
of everystep/2call. Lets server-side concurrency tests deterministically keep a
holder request in-flight while other requests race for the queue (used by the e2e
suite'schat_busy_returns_429to fix a pre-existing CI timing flake where the
holder's stream could finish before the racing requests arrived). Default 0 is a
true no-op for the cache / integration tests that already use the stub. Invalid
configs coerce to 0.
Fixed
- Idle sticky-session seq pins are now reclaimed under seq-pool pressure, so the pool no
longer permanently exhausts. A completed sticky turn keeps its sequence pinned for warm
continuation, but those pins were never released (onlyend_session/2or model stop
freed them) - so aftern_seq_maxdistinct sessions, every new session got
{error, seq_capacity}(529) with retries never recovering. Admission now reclaims the
least-recently-used idle pin (asession_seqentry whose seq has no in-flight
request) when the pool is full, on both the fresh-admit (admit_normal/2) and queued-
dispatch (dispatch_pending_admits/2) paths, while never reclaiming an in-flight
session.seq_capacitynow only fires when every seq is genuinely active. A reclaimed
session re-admits cold (or warm-restores from the tiered cache) on its next turn.
model_info/1gainspinned_idle_seqs(reclaimable headroom;available_seqsstays
~0 since an admitted request immediately re-pins). - Native tool-call capture now keeps the body between the markers. The marker scanner
(erllama_model_llama:map_marker/2) is stateless - it tags only the start/end
marke...
v0.8.0
[0.8.0] - 2026-05-23
Engine-robustness release covering the erllama_server hardening brief observed under real 30B/Metal load (cold-admit decode wedges, agentic tool-continue loops).
Added
- Bounded, interruptible, self-recovering decode. Every context installs a ggml abort callback. Each decode step arms a per-step wall-clock budget (
context_opts.decode_budget_ms, default 30000, 0 disables); exceeding it returns{error, decode_timeout}instead of blocking.erllama_nif:request_abort/1sets an atomic flag the callback honours without taking the context mutex, so a running decode can be interrupted from outside the blocked gen_statem and returns{error, decode_aborted};cancel/1fires it best-effort. Ondecode_timeout/decode_abortedthe engine recovers in place: fails in-flight and queued callers, recreates the context via the new backendreset_context/1(model stays loaded), resets seq and session state, returns to idle. Recovery drops only the live in-context KV and sticky-session pins; the persistent tiered cache survives, so the next admission still warm-restores from cache rather than starting fully cold. on_full => block | erroradmission option oncomplete/3,prefill_only/3,infer/4(defaultblock).errorfails fast with{error, seq_capacity}instead of queueing when no seq is free; pair withavailable_seqs/n_seq_maxfrommodel_info/1.generated => [token_id()]in theerllama_doneStats map: the exact generated token ids in order, so a caller can build a byte-exact suffix forcontinue/3without re-tokenising detokenised text.expect_committed => [token_id()]option oncontinue/3: the caller's view of the session's committed tokens. When supplied it must equal the stored context exactly, otherwisecontinue/3returns{error, {transcript_mismatch, #{stored_len, expected_len, diverge_at}}}without prefilling, leaving the seq pinned for a re-sync and retry.
Changed
- A binary
grammaris now authoritative through tool-call syntax tokens. Previously, on a model withtool_call_markers, atool_choice=required/response_formatgrammar was abandoned once a tool-call span opened (syntax tokens went through a grammar-less greedy sampler), letting output drift to free-form. The greedy-on-syntax swap is now disabled for any request carrying a grammar, so the constraint holds end to end oninfer/4andcontinue/3. nif_decode_onenow returns{error, {decode_failed, Rc}}instead of a bare{error, Rc}, matchingnif_step.
v0.7.0
[0.7.0] - 2026-05-20
Added
erllama:reset_session/2recovery primitive. Forcibly drops a
sticky session's live KV cells and any in-flight#req{}on its
seq, then returns the seq slot to the idle pool. Uses a 5 s
gen_statem:calltimeout so it stays reachable when the engine's
infinity-timeout hot path is wedged. Returns
{ok, recovered | not_found} | {error, timeout}. Streaming
callers on the reset seq receive{erllama_error, Ref, engine_reset}.n_seq_maxandavailable_seqskeys inerllama:model_info/1.
available_seqsis the live idle-list length; sticky-pinned seqs
count as unavailable. Lets callers detect saturation up front
instead of inferring it fromsticky_busyerrors.
Changed
nif_stepnow returns{error, {decode_failed, Rc}}(was bare
{error, decode_failed}). TheRcinteger surfaces the
llama_decodereturn code (1, -1, 2, ...) so operators can tell
OOM from KV-cache corruption from sample rejection. The exception
path ({error, exception}) is unchanged.
[0.6.2] - 2026-05-19
Changed
- Vendored llama.cpp bumped from
b9119tob9222. No erllama
source changes required; the C ABI surfaces we depend on (model
load/free, context, sampler chain,llama_n_batch,
llama_state_seq_*) are unchanged.
v0.6.1
[0.6.1] - 2026-05-18
Fixed
- BEAM segfault in
erllama:apply_chat_template/2on rendered
chat-template output above the initial 4 KiB render buffer. The
vendoredllama_chat_apply_templatereturns the full formatted
size as a positive value even when the caller's buffer was too
small (strncpysilently truncates). The NIF retry path only
fired on negative return, so the positive size was fed as
text_lentollama_tokenizeand the subsequentstd::string
construction walked past the truncated buffer into unmapped pages.
Retry now triggers onwritten > buf_size. Caps bumped to match
the downstream's 64 MiB body limit:ERLLAMA_MAX_TOKEN_TEXT
4 MiB → 64 MiB,ERLLAMA_MAX_TOKENS1 M → 16 M. The token output
cap is enforced on the tokenize success path so byte-fallback
tokenizers don't return over-cap lists.
v0.6.0
[0.6.0] - 2026-05-18
Added
erllama:continue/3for caller-asserted chat-template continuation
on a sticky session. Skips the prompt prefix-equality check in
resolve_sticky_continuationso chat templates whose rendered
prefix shifts between turns can still reuse the live KV cells.
Caller passes the tokenised tail directly; the engine prefills
only that tail on top of the pinned seq's stored KV. Returns
{error, no_session}for unknown session ids and
{error, sticky_busy}for in-flight seqs. New
cache_hit_kind => continuationreports the path inStats.
Companion guide section inguides/examples.mdand lifecycle
notes ininternals/request-lifecycle.md.
v0.5.1
[0.5.1] - 2026-05-17
Documentation-only patch on top of 0.5.0.
Added
## Tool-call handlingsection in the README describing what
erllama exposes (per-modeltool_call_markers, the
{tool_call_delta, _}/{erllama_tool_call_end, _, Full}
streaming wire, and the automatic greedy-on-syntax sampler
swap) and what it deliberately leaves to the HTTP layer (tool
id minting, JSON parsing, canonicalisation).- New
guides/tool-calls.mdcompanion to the README section,
linked from the documentation table. - New
internals/request-lifecycle.mddescribing the per-model
gen_statemadmission, cache resolution, decode, and save
pipeline. internals/c-safety-audit.mdadded to the HexDocs navigation.
Changed
- README rewritten for sharper top-of-funnel: tighter "Why" list,
cleaner Quick taste, Common patterns block replacing the old
long example. - Architecture diagram corrected —
erllama_cache_ramfile_srv
anderllama_cache_disk_srvare operator-started standalone
servers, not children oferllama_cache_sup. Added
erllama_registryanderllama_inflightwhich were missing. - "Inside a request" lifecycle updated for the multi-seq
scheduler: two states (idle/running) instead of the v0.1
three-phase model, with co-batchednif_stepand inline
thinking/tool-call marker recognition.
v0.5.0
[0.5.0] - 2026-05-16
Tool-call exact-replay scaffolding for downstream HTTP front ends.
Models loaded with tool_call_markers produce structured boundary
messages on the streaming wire so a caller can capture the exact
bytes the model sampled, store them under a tool id, and splice
them back verbatim on later turns to keep the KV-cache prefix
match working. Companion primitives expose explicit suffix replay
and sticky per-session seq_id pinning.
Added
tool_call_markers => #{start, end, payload_start (optional), payload_end (optional)}onerllama:load_model/2Config. Each
binary is tokenised through the model's own vocabulary at load
time; multi-token markers are supported. Omitting the key keeps
the backend on the existing path (#39, #42).- Streaming wire on
infer/4gains
{erllama_token, Ref, {tool_call_delta, Bin}}per chunk and a
single{erllama_tool_call_end, Ref, Full :: binary()}per
span, withFullcarrying every emitted delta concatenated so
the downstream's exact-replay map stores them verbatim without
re-buffering (#39). erllama:prefill_only/3acceptingOptswithparent_key.
When passed a prior turn'sfinish_key, the call warm-restores
from that row and prefills only the new suffix before firing the
finish save — useful for chaining cache-warming calls across
turns (#40).session_id => term()oninfer/4Params and
complete/3/prefill_only/3Opts. Pins the underlying seq_id
to that session across requests so the next turn whose prompt
continues the stored tokens truncates-and-prefills in place on
the already-live KV cells (cache_hit_kind => sticky).
Concurrent admits on the samesession_idreturn{error, sticky_busy}. Release witherllama:end_session/2(#41).- Per-request greedy sampler swap on tool-call syntax tokens.
Models withtool_call_markersbuild a second sampler chain
(temperature => 0) at admission; the scheduler routes syntax
tokens through it so a tool call is byte-deterministic from a
fixed prefix. Optional payload markers flip back to the request's
normal sampler for caller-supplied string contents so they stay
diverse (#42).
Changed
step_result()onerllama_model_backendgains four variants
({tool_call_token, _},tool_call_end,{tool_call_payload_open, _},{tool_call_payload_close, _}). Backends without
tool-call markers emit none of them.erllama_model_stubphase machine re-keyed from sampler ref onto
seq_id so mid-request sampler swaps (the new greedy-on-syntax
path) don't reset state across ticks.seq_rmnow cleans the
per-seq phase entry.