contenox
Browse docs/

llama.cpp Binding Decision: Contenox-Owned Shim

Date: 2026-06-16

Status: final decision record.

Decision

The llama.cpp blocker is resolved by building a minimal Contenox-owned llama.cpp shim for llama.

This means:

yes: own the small CGo/C shim needed by Contenox llama
yes: expose only the llama.cpp primitives llama needs
yes: keep product code behind the backend-neutral llama session API

no: unsafe ABI access to Ollama's private Go struct
no: long-term fork of Ollama's wrapper
no: server sidecar
no: relying on Ollama's OpenAI-compatible/tool-call behavior for llama

Ollama’s Go wrapper remains a temporary bring-up dependency only. It is not the permanent substrate for the graduated local coding node.

Why The Ollama Wrapper Blocks Us

runtime/modelrepo/llama/llamasession currently uses:

import "github.com/ollama/ollama/llama"

That wrapper exposes enough for early live prefix reuse:

Model.Tokenize
Context.Decode
Context.KvCacheSeqRm
Context.KvCacheSeqCp
Context.KvCacheSeqAdd
Batch
sampler helpers

But it does not expose the ownership primitives llama needs:

llama_free(ctx)
llama_state_seq_get_size
llama_state_seq_get_data
llama_state_seq_set_data
llama_state_seq_save_file
llama_state_seq_load_file

It also collapses positive llama_decode return codes into generic failure. Upstream llama.cpp distinguishes:

0      success
1      no KV slot available
2      aborted; partially processed ubatches may remain
-1     invalid input batch
< -1   fatal error

The llama runtime needs those distinctions for cancellation, rollback, fatal-session eviction, and honest context accounting. A partially processed prefill cannot be treated as a generic KV-full error.

Rejected Paths

Unsafe ABI Shim

Rejected as the final path and no longer the L0 plan.

The unsafe route would mirror the beginning of Ollama’s private llama.Context layout and read the unexported C pointer:

type Context struct {
    c          *C.struct_llama_context
    numThreads int
}

That is too fragile for the runtime core. It can compile while being wrong if Ollama changes layout, and the failure mode is memory corruption, double free, or bad snapshots. It also risks duplicate llama.cpp linkage if not fenced perfectly.

Unsafe was useful as a thought experiment because it clarified the missing ABI. It is not the implementation path.

Fork / Replace Ollama

Rejected as the main path.

Adding wrappers inside Ollama’s llama package would solve visibility, but it means carrying a fork or a heavy vendored replacement for a whole application runtime when llama only needs a small subset of llama.cpp.

The fork has lower memory-safety risk than unsafe ABI, but higher maintenance weight than owning a focused shim.

Upstream PR Only

Not a blocker strategy.

An upstream PR may still be useful later, but release timing and API acceptance are outside Contenox control. The llama runtime needs an owned path.

Owned Shim Scope

Create a small package that owns the llama.cpp C API boundary for llama:

runtime/modelrepo/llama/llamacppshim/
  model.go
  context.go
  batch.go
  sampler.go
  state.go
  lifecycle.go
  errors.go

The exact package name can change, but the ownership boundary should not: product code talks to llama.Session; llama-specific code talks to the shim.

Expose only the required API:

type Model struct{}
type Context struct{}
type Batch struct{}
type Sampler struct{}

func LoadModel(path string, cfg ModelConfig) (*Model, error)
func NewContext(model *Model, cfg ContextConfig) (*Context, error)
func (c *Context) Free() error
func (m *Model) Free() error

func (m *Model) Tokenize(text string, addSpecial, parseSpecial bool) ([]int, error)
func (m *Model) TokenToPiece(id int) (string, error)
func (m *Model) TokenIsEOG(id int) bool

func NewBatch(capacity int) (*Batch, error)
func (b *Batch) Free() error
func (b *Batch) Clear()
func (b *Batch) Add(token, pos, seqID int, logits bool) error

func (c *Context) Decode(batch *Batch) DecodeResult
func (c *Context) KVSeqRemove(seqID, p0, p1 int) error
func (c *Context) KVSeqCopy(src, dst, p0, p1 int) error
func (c *Context) KVSeqAdd(seqID, p0, p1, delta int) error

func (c *Context) StateSeqGetData(seqID int) ([]byte, error)
func (c *Context) StateSeqSetData(seqID int, data []byte) error
func (c *Context) StateSeqSaveFile(path string, seqID int, tokens []int) error
func (c *Context) StateSeqLoadFile(path string, seqID int, tokenCapacity int) ([]int, error)

DecodeResult must preserve llama.cpp’s real status:

type DecodeStatus string

const (
    DecodeOK          DecodeStatus = "ok"
    DecodeNoKVSlot    DecodeStatus = "no_kv_slot"
    DecodeAborted     DecodeStatus = "aborted_partial"
    DecodeInvalid     DecodeStatus = "invalid_batch"
    DecodeFatal       DecodeStatus = "fatal"
)

type DecodeResult struct {
    Status DecodeStatus
    Err    error
}

Build Requirements

The shim must own its build/link story explicitly:

default build:
  no native llama.cpp requirement

llamanode build tag:
  builds the Contenox shim
  links one llama.cpp copy
  exposes the llama adapter

tests:
  tiny GGUF only by default
  larger models opt-in through env vars

No duplicate llama.cpp copies may be linked into the same binary. The L0 test must report linked mode and runtime version through ExplainContext or benchmark metadata.

Integration Plan

L0 - Owned Shim Round-Trip

Build the shim first. Then prove:

load tiny GGUF
create model/context/batch/sampler
tokenize prompt
prefill prompt
save seq state
create fresh context
load seq state
continue greedy decode
restored continuation equals original
close context/model without leak or crash

Also record:

state bytes
save ms
restore ms
decode status
llama.cpp version/build metadata

L1 - Port llamasession

Replace direct use of github.com/ollama/ollama/llama inside the graduated llama adapter. The implementation target is the single runtime/modelrepo/llama package; the old runtime/modelrepo/local and runtime/modelrepo/localnode packages are retired. Compatibility is a backend keyword shim only: modelrepo.CanonicalBackendType("local") == "llama".

L2 - Warm Prefix / Suffix Equivalence

Use the same owned shim to prove:

warm stable prefix + suffix == cold full prompt
edited stable segment causes precise miss/reuse behavior
decode cancellation yields valid or explicitly fatal session
failed suffix prefill rolls back or marks session fatal

L3 - Snapshot Policy

Only after L0/L2 numbers exist, decide whether snapshots are hot-path useful or only durability/branching/reproduction tools. The hot path remains live prefix reuse unless measurements prove restore is better.

Required Llama Contracts

The shim does not leak into product code. It feeds the existing llama primitives:

EnsurePrefix(ctx, PrefixInput{Text, Manifest}) -> PrefixStatus
PrefillSuffix(ctx, SuffixInput{Text, Manifest}) -> SuffixStatus
Decode(ctx, DecodeConfig) -> StreamChunk
ExplainContext() -> ContextReport
Close() error

Compatibility rules before restore or prefix reuse:

model digest must match
backend version must match
prompt template digest must match
runtime digest must match
BOS policy must match
stable token hash must match or restore is refused

Kill Gates

Do not graduate the shim until all pass:

tiny GGUF <=512 MiB
state get returns non-empty bytes
restore into fresh context succeeds
one-token greedy continuation equals original
multi-token greedy continuation equals original
restore with wrong manifest is rejected
double close does not crash
cancel during prefill/decode leaves session valid or fatal, never ambiguous
no duplicate llama.cpp link/copy
decode status preserves aborted/no-slot/invalid/fatal distinctions

Minimum report:

{
  "model": "tiny",
  "backend": "llamacpp",
  "mode": "contenox_owned_shim",
  "prefix_tokens": 123,
  "state_bytes": 456789,
  "save_ms": 12,
  "restore_ms": 18,
  "continuation_equal": true,
  "decode_status_mapping": "preserved",
  "close_mode": "llama_free",
  "duplicate_llama_link": false,
  "notes": []
}

Current Action

Build the Contenox-owned shim. Do not spend more implementation time on unsafe ABI access or an Ollama fork unless the owned shim hits a concrete blocker.

Esc to close