Blog

Ardana, Ollama 0.35 and llama.cpp for local decision models

A support ticket through ardana run, the documented requests for Ollama 0.35 and the llama.cpp server, what each returns, and which models each one can run.

A small groundhog feeds copies of one paper ticket into three different machines on a desk, each machine showing its own dial.

Three local servers now speak /v1/systemone

Ollama 0.35 shipped /v1/systemone on September 29, 2026, with nimble and tev1 behind it. Three days later, on October 2, llama.cpp merged PR #29818 and llama-server got the same route with five day-0 GGUFs. So one state-and-questions shape now runs on 127.0.0.1:8000 for ardana serve, localhost:11434 for Ollama, and 127.0.0.1:8080 for llama.cpp: state, questions, the three types choice, score and noul, one path. Ardana takes choice criteria as a plain array; Ollama and llama.cpp want an object mapping each option to a description, or to null.

I built Ardana around this exact shape, so of-course the obvious test is the same ticket in front of all three. It is the README ticket: a customer charged twice for order A-104 who wants the duplicate refunded. Two questions ride with it, a choice over billing, technical and sales, and a noul for whether the customer asks for a refund. Ardana’s server comes up with one command.

ardana serve

Then the body goes to it.

curl http://127.0.0.1:8000/v1/systemone -H 'content-type: application/json' -d '{"model": "decider:2b", "state": "My card was charged twice for order A-104. Please refund the duplicate.", "questions": {"department": {"type": "choice", "instructions": "Which department should handle this?", "criteria": ["billing", "technical", "sales"]}, "refund": {"type": "noul", "instructions": "Does the customer ask for a refund?"}}}'

On Ollama the model comes first.

ollama pull nimble

For Ollama and llama.cpp, the criteria array has to become an object. What each prints back is where they stop looking alike.

The ticket through ardana run

I start with the README ticket. A customer writes “My card was charged twice for order A-104. Please refund the duplicate.” and I want two things: the department, and whether a refund was asked for. With ardana run those two questions are two flags.

ardana run decider:2b "My card was charged twice for order A-104. Please refund the duplicate." 
  --choice "Which department should handle this?" billing technical sales 
  --noul "Does the customer ask for a refund?" 
  --verbose

--choice takes a question and then two or more options. --noul takes a yes/no question and answers with the probability of yes. There is also --score for 2 to 10 levels, lowest first, and --request <FILE> for a whole /v1/systemone body. Flags become q1, q2 in command-line order, so --choice here is q1 and --noul is q2. Under the hood --choice turns into the first shape below and --noul into the second:

{"type":"choice","instructions":...,"criteria":[options]}
{"type":"noul","instructions":...}

Here is what it printed on my machine, an AMD Ryzen 7 3800X with no GPU, ardana 0.2.2 and decider:2b at Q4_K_M:

Which department should handle this?
  * billing    93.2%
    technical   0.5%
    sales       6.4%
    confidence 0.90, p max 93.2%, certainty 0.76

Does the customer ask for a refund?
    probability of yes  0.93

The pick is marked *, probabilities are percent with one decimal, and after the option lines the choice question gets one more line: confidence, p max and certainty. The README’s sample has billing at 93.5%, mine says 93.2%. Same pick, a few tenths apart. --verbose put the model, token counts and timings on stderr: model: decider-2b-v11, input tokens: 72, output tokens: 0, load duration: 1.48s, answer duration: 1.09s, total duration: 3.38s.

Then the same command with --json added, which prints the API body, extras under their API names, x_p_max and x_certainty:

ardana run decider:2b "My card was charged twice for order A-104. Please refund the duplicate." 
  --choice "Which department should handle this?" billing technical sales 
  --noul "Does the customer ask for a refund?" 
  --verbose --json
{
	"model": "decider-2b-v11",
	"answers": {
		"q1": {
			"type": "choice",
			"choice": "billing",
			"confidence": 0.8973,
			"probabilities": { "billing": 0.9316, "technical": 0.0047, "sales": 0.0637 },
			"x_p_max": 0.9316,
			"x_certainty": 0.7571
		},
		"q2": { "type": "noul", "noul": 0.9255 }
	},
	"usage": { "input_tokens": 72, "output_tokens": 0 }
}

That run loaded the model again: load 1.53s, answer 1.51s, total 3.91s. Each is one cold run on a CPU. It is not a benchmark. The README says about a tenth of a second on an M1 MacBook once the model is there; the 1.3 GB download is first run only.

So what was it running?

ardana show decider:2b

It prints layout plain (decider format), runtime llama.cpp, released 2026-09-24, and the calibration it shipped with: choice temperature 1.164, noul 1.624, score 1.124. Ollama and llama.cpp each document their own request, with their own ticket.

What Ollama and llama.cpp document

Ollama is not installed here, so for these two I take the request each project ships in its docs. Both tickets start from a customer charged twice. Ollama’s asks for a refund; llama.cpp’s says nobody has replied and asks whether the customer is angry instead. The Ollama blog sends this with model nimble; the API reference puts the endpoint at POST /v1/systemone on http://localhost:11434.

{
	"model": "nimble",
	"state": { "ticket": "I was charged twice. Please refund the extra payment." },
	"questions": {
		"team": {
			"type": "choice",
			"instructions": "Which team should handle this ticket?",
			"criteria": {
				"billing": "Payments and refunds",
				"technical": "Bugs and integrations",
				"other": "None of the above"
			}
		},
		"refund": { "type": "noul", "instructions": "Does the customer explicitly ask for a refund?" },
		"urgency": {
			"type": "score",
			"instructions": "How urgent is this ticket?",
			"criteria": ["Routine", "Soon", "Urgent"]
		}
	}
}

The blog’s answer picks billing at 0.985, refund 0.997, urgency score 0.815, on 841 input tokens and 4 output tokens.

llama.cpp’s README curl goes to port 8080 with no model field, options {"billing": null, "shipping": null, "technical": null}. The response, which the README says is shortened, names openjev anyway.

{
	"model": "openjev",
	"answers": {
		"route": {
			"type": "choice",
			"choice": "billing",
			"probabilities": { "billing": 0.9998, "shipping": 0.0001, "technical": 0.0001 },
			"confidence": 0.9997
		},
		"angry": { "type": "noul", "noul": 0.6328 },
		"urgency": {
			"type": "score",
			"score": 2.0858,
			"legend": { "0": "can wait", "1": "this week", "2": "today", "3": "right now" },
			"probabilities": { "0": 0.0023, "1": 0.116, "2": 0.6753, "3": 0.2064 },
			"confidence": 0.673
		}
	},
	"usage": { "input_tokens": 239, "output_tokens": 0 }
}

Both take the choice options as an object, option to description, and the description can be null. Ardana takes a plain array. Ollama also requires model in its API reference, while llama.cpp’s example has none. So the endpoint name is the same, but the body comes in three dialects.

Then output_tokens. Ollama documents it as “Tokens generated internally for scoring”, hence the 4. llama.cpp says it “is always 0”. Ardana’s README answer is 0 too.

Limits differ. Ollama’s API reference takes 1 to 64 questions, a 64 KiB body, 2 to 255 choice options and 2 to 26 score levels, while the table in its guide gives 2 to 26 for both. llama.cpp takes 2 to 10 score levels and caps the options by the model: 52 for openjev, 255 for laya. Ardana sits at 2 to 255 options and 2 to 10 levels.

The line I would keep from the llama.cpp README: “The probabilities are scaled with the temperatures stored in the model file. They are not guaranteed to be calibrated for your data.” That is honest.

Calibration is a property of the model, not of the server. Three servers can speak the same JSON and still hand you probabilities that mean different things.

Neither response carries anything past confidence. Which models each server can load is the bigger split, and that is where the formats stop crossing.

Who runs which models

All three servers take a request of the same shape. Ardana still does not read nimble, tev1 or the llama.cpp day-0 GGUFs. Ollama runs nimble, a 9B model from Bespoke Labs fine-tuned from Qwen3.5-9B, 9.3 to 9.5 GB under Apache 2.0. It also runs tev1, Together AI’s experimental family in 4B (4.4 to 4.5 GB) and 0.8b (797 to 812 MB). The tev1 page says the dataset builders and training scripts are MIT and says nothing about the weights.

llama.cpp ships five day-0 GGUFs, switched on by {arch}.decision.type in the GGUF metadata:

llama-server -hf ggml-org/OpenJev-GGUF
llama-server -hf ggml-org/lev-GGUF
llama-server -hf ggml-org/Laya-GGUF
llama-server -hf ggml-org/Julia-1-GGUF
llama-server -hf ggml-org/Kev-4B-GGUF

OpenJev is the heavy one, 18,973,872,288 bytes at Q4_K_M under cc-by-nc-4.0. The other four are Apache-2.0: lev 3,011,777,440 bytes and Kev-4B 3,033,489,824 at Q4_K_M, Laya 449,397,600 and Julia-1 168,166,496 at Q8_0. Ardana reads none of these today, and not nimble or tev1 either. There is no decision-head code in our crates. I would rather say it plainly than have you find out from an error.

Ardana reads two kinds of GGUF. A decider-format GGUF that ships with decider_config.json goes into the plain layout, the state-first layout every decider model is trained on. A stock instruct GGUF without one goes into the chat layout: the tokenizer’s chat template around one user turn, read zero-shot with the stock profile at temperature 1.0. That is how gemma-4, qwen3.5, qwen3.6, qwen3.8 and smollm3 run. Any hf.co/<org>/<repo> GGUF, a local .gguf or a model from the Ollama store is read in place:

ardana pull ollama:llama3.2

A pull from the Ollama store gets only the stock chat profile. decider itself comes in three sizes on Qwen3.5 base models, all Apache-2.0: decider:0.8b at q8_0 only (811,843,552 bytes), decider:2b in q4_k_m, q8_0 and bf16 (1,274,396,800 up to 3,775,709,312 bytes, q4_k_m the default) and decider:4b in the same three quants (2,708,804,640 up to 8,424,393,760 bytes). ardana pull decider:2b-q8_0 picks a quant, and the family name alone picks its latest size.

What Ardana puts on top of the endpoint

With the request shape shared, the difference is in what comes back. Ardana’s choice answer carries x_p_max and x_certainty next to confidence and the probabilities, and the score answer adds x_level_fit and x_fit_mass on top. In the terminal the same thing reads as one line under the pick: confidence 0.90, p max 93.2%, certainty 0.76. P max and certainty are what only Ardana’s answer has.

A probability is only useful if you know how much to trust it. Certainty is our answer to that, and it ships in the response, not in a doc.

Calibration lives in the model, so I made it visible. ardana show decider:2b prints the temperatures the probabilities are scaled with, one per question type: choice 1.164, noul 1.624, score 1.124. The 2b and 4b sizes come in q4_k_m, q8_0 and bf16, the 0.8b only in q8_0, and all of them are Apache-2.0.

ardana serve also bundles a playground at /. It shows every probability, the x_ extras, token usage, latency, the raw request and response, and copies the request as curl, Python with typesafe-sdk or TypeScript with @typesafe-ai/sdk. Every value on screen matches the API response it came from. On ardana.ai/playground/ the same decider:0.8b, decider:2b and qwen3.5:0.8b run inside the tab as ONNX on WebGPU, falling back to WASM; decider:0.8b int4 is 467,748,928 bytes to download.

One binary lands in ~/.local/bin:

curl -fsSL https://ardana.ai/install.sh | sh
irm https://ardana.ai/install.ps1 | iex
ardana run decider:2b "My card was charged twice for order A-104. Please refund the duplicate." --choice "Which department should handle this?" billing technical sales --noul "Does the customer ask for a refund?"

Tell me what your ticket gets back.