You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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' enables erlang: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's common_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. Unused vendor/
libraries are pruned and LLAMA_OPENSSL is off (no OpenSSL link).
Hex package manifest now includes c_src/erllama_chat_nif.{cpp,h}
and c_src/erllama_resources.h (previous tarballs could not build).
Bump vendored llama.cpp from b9334 to b9585. No API-breaking changes
on our touchpoints; brings common/chat* bug fixes (LFM2 reasoning,
tool-parser unification). UPDATE_LLAMA.md refreshed to document
the common/ + vendor/ sync the prior procedure left implicit.
Removed (BREAKING)
Streaming API: {erllama_token, _, {tool_call_delta, _}}' and erllama_tool_call_end' messages, plus the matching
step result variants. The engine no longer classifies tool-call
bytes mid-stream. Callers using erllama:infer/4' directly buffer tokens and call erllama: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' becomes chat_apply/2' (drops the ToolsHash' parameter). erllama_chat_cache' shrinks to
caching only the heavy common_chat_templates_init' ref per model; each request invokes common_chat_templates_apply' fresh because
the synthesized parser is sensitive to tool_choice' and parallel_tool_calls' (now folded into the NIF Inputs map).
Added
Public erllama:chat_apply/2 + chat_parse/3 that delegate
to erllama_chat via the model gen_statem so callers can
build a chat_params_ref' and parse model output without touching the underlying NIF model resource. Backend gains an optional get_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/3 unconditionally; for Granite / Phi-4 the EOS
is a special token that detokenizes empty under special=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 calls req_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 + tools JSON
binaries, optional tool_choice) into common_chat_templates_inputs
via common_chat_msgs_parse_oaicompat / common_chat_tools_parse_oaicompat,
calls upstream, and returns the synthesized chat_params_ref plus
the rendered prompt bytes. nif_chat_parse deserialises the
per-template PEG arena from the cached params string and dispatches
to common_chat_parse. The parsed common_chat_msg is 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).
New erllama_chat_SUITE real-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-httplib from the
pinned llama.cpp tree, flips LLAMA_BUILD_COMMON=ON, and links llama-common into 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_ptr and common_chat_params. New Erlang
facade erllama_chat (raw NIF shim) and erllama_chat_cache (LRU cache keyed on {ModelIdBin, ToolsHash}, with purge/1 for model unload). The
refactored erllama_resources.h exposes the existing
C resource type pointers + erllama_model_t to C++
TUs behind an extern "C" guard. templates_init works
end-to-end; templates_apply and parse ship 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 with EogFlag = 1, the
accumulated tool_call_bytes buffer is flushed via the existing erllama_tool_call_end message before the request
finishes. Previously the buffer was silently dropped in that
branch (the only emission site was the byte-string end-marker
match in erllama_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 the end' key. Backend behaviour gains one optional tool_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_end message, which the server parsed as one garbled
call. apply_step_results/2 now 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
optional tool_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/2 was discarding EogFlag on 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 = true on eog so the turn ends cleanly. Backend type spec updated
accordingly.
erllama_model_stub gains an opt-in step_delay_ms :: non_neg_integer()
test knob that timer:sleeps for the configured number of milliseconds at the top
of every step/2 call. Lets server-side concurrency tests deterministically keep a
holder request in-flight while other requests race for the queue (used by the e2e
suite's chat_busy_returns_429 to 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 (only end_session/2 or model stop
freed them) - so after n_seq_max distinct sessions, every new session got {error, seq_capacity} (529) with retries never recovering. Admission now reclaims the
least-recently-used idle pin (a session_seq entry 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_capacity now 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/1 gains pinned_idle_seqs (reclaimable headroom; available_seqs stays
~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
marker tokens - so for a model whose tool-call body is ordinary tokens (e.g.
Qwen3-Coder's <tool_call><function=NAME><parameter=P>v</parameter></function></tool_call>)
the body was streamed as content and the captured tool_call_bytes held only the
start marker, yielding an empty call (name "unknown", arguments {}). The decode
loop now accumulates any token sampled while a tool-call span is open
(active_sampler = tool_call_syntax / tool_call_payload) into the tool-call bytes
instead of the content stream, so the full call reaches the parser. Models that emit
their call without the marker tokens (e.g. Qwen2.5 qwen-xml JSON) are unaffected -
they never open a span.
Added
Scheduler can proactively unload an idle model under sustained memory pressure
(opt-in scheduler.unload_models_under_pressure => true). After cache eviction runs,
if it could not free the target and pressure is still high, the scheduler calls the
configured model_evictor (new erllama_model_evictor behaviour, evict_one/0) to unload the least-recently-active idle model. Cache slabs are always
freed first; at most one model is unloaded per tick; the callback's return is
validated so a missing or misbehaving evictor degrades to no-op. status/0 reports models_unloaded_total / last_model_unloaded. The engine names the evictor module
via config only - no compile-time dependency on the server app that implements it.
Changed
Vendored llama.cpp bumped from b9222 to b9334 (ggml 0.12.0 -> 0.13.0). No erllama
source changes required; the C ABI surfaces the NIF wraps
(erllama_safe.cpp: model load/free, context, sampler chain, llama_decode, llama_memory_seq_*, llama_state_seq_*) are unchanged.
Cache eviction is now frecency-scored, not pure LRU. Byte-targeted eviction
(evict_bytes, scheduler memory pressure) drops the lowest-scoring rows first, where
the score is recency biased forward by the row's hit count with a 6 h half-life decay
(erllama_cache_meta_srv:eviction_score/3). Pinned static-prefix rows and the
currently-live session key (set_live_key/1, ds4 protected_sha) sort in a higher
rank so they survive gc/0 and are evicted by evict_bytes only as a last resort
when nothing else frees enough. The old one-shot last_used += hits*1s install bias
is removed (hits now feed the score directly, so they are no longer double-counted).
Added
Pinned static-prefix (agent_prefix) checkpoints. When a caller supplies a
verified prefix_checkpoint_len (the end-of-tools token offset), the cold prefill
writes an agent_prefix KV checkpoint at exactly that boundary - independent of the cold_min/cold_max band - and pins it so the hot, shared system+tools prefix is not
evicted under churn. The pin is bounded to one row per namespace
(erllama_cache_key:namespace/3), survives restart (recovered from the
persisted save reason on the disk scan), and is re-applied on a warm resume that
lands on the boundary. New erllama_cache_meta_srv:pin_row/2, ?POS_PINNED
row field, ?C_SAVES_AGENT_PREFIX counter (saves_agent_prefix), and KVC save
reason 6 (mirrors ds4 AGENT_SYSTEM). Cold-prefill segments now carry a per-boundary
save reason.
Changed
KV cache is now keyed by the rendered prompt bytes
(detokenize(tokens)), not the token-id list (ds4-style,
content-addressed). The same logical prompt now hits across turns
even when it retokenises (chat-template wrapping, tool rendering,
generated ids vs re-tokenised assistant text), where the old
token-keyed cache went cold every turn. The longest-prefix lookup
(erllama_cache_meta_srv:lookup_longest_text_prefix/2)
scans stored byte-prefix lengths and resumes from the longest match.
The uncovered suffix reuses the caller's original tokens when the
byte boundary lands on a token boundary (token-exact resume - so a
re-sent prompt still reproduces its reply); only a mid-token boundary
re-tokenises the byte remainder (checkpoint_tokens ++ tokenize(byte_suffix), identical byte stream, sound). Hits stay
exact (SHA-256 over the bytes; no fuzzy match). The KVC file format
version is bumped to v2; old v1 (token-keyed) files are rejected
on the startup disk scan, so the cache refills under the new scheme. erllama:list_cached_prefixes/2 now reports the matched byte length.
Added
Per-context compiled-grammar cache. An identical GBNF (agentic clients like
Claude Code resend the same tool grammar every turn) is now parsed once and
cloned per request via llama_sampler_clone instead of re-parsed, which
dominated infer admission for large tool grammars. The cache is a small
byte-verified LRU on the context resource (a hash is only a pre-filter; identity
is confirmed by length + memcmp), freed with the context. New erllama_nif:grammar_cache_stats/1 (#{hits, misses}) exposes whether
the cache is taking effect.