10. The ornotto package¶
ornotto is the Python side of this book. It speaks System One (a state, plus named questions of three kinds) to the local engines, starts and stops them for you, and downloads the models they need. Two engines, dohnuts and pcdServer, ship inside the wheel; ollaya and the native Laya MLX/xDecision runtimes are separate installs that ornotto drives when it finds it. This chapter is the reference: what each object takes, what it returns, and what happens underneath.
Install¶
uv add ornotto # or: pip install ornotto
uv add "ornotto[pydantic-ai]" # adds pydantic-ai-slim[typesafe] for chapter 11
Every wheel on PyPI contains dohnuts-cli, built from dohnuts.cpp, and all but the Windows wheel also contain pcd_server, built from pcdServer. Both are compiled statically from the engines/ submodules. On Windows, the pcd-only models (qwen3.5-0.8b, qwen3.5-2b, qwen3.5-4b, qwen3.5-4b-hmm) and any engine="pcd" raise EngineNotFound unless ORNOTTO_PCD_BIN points at a pcd_server you built yourself.
| Platform | Wheel tag | Backend |
|---|---|---|
| macOS 14 or later, Apple silicon | macosx_14_0_arm64 |
Metal |
| Linux x86_64, glibc 2.28 or later | manylinux_2_28_x86_64 |
CPU only (no CUDA or Vulkan) |
| Linux aarch64, glibc 2.28 or later | manylinux_2_28_aarch64 |
CPU only |
| Windows x64 | win_amd64 |
CPU; dohnuts only, because pcdServer's sources do not build on Windows yet |
You need Python 3.10 or later. The Python code is the same on every platform. Only the executables differ, so each wheel is tagged py3-none-<platform>.
If no wheel fits your platform, pip falls back to the source distribution, which carries no engine sources and installs the Python code alone. ornotto then looks for each engine in three places, in this order:
- The path in
ORNOTTO_DOHNUTS_BINorORNOTTO_PCD_BIN. - The copy bundled in the wheel, under
ornotto/_bin/. dohnuts-cliorpcd_serveron yourPATH.
If none of them exists, the first question raises EngineNotFound with the name of the variable to set. Chapter 12 shows how to build both engines yourself.
ollaya¶
ollaya is not in the wheel and not on PyPI: it is one Rust binary that fetches its models from its own registry. Install it without sudo into ~/.local:
ornotto looks for it at the path in ORNOTTO_OLLAYA_BIN, then on your PATH, then in ~/.local/bin. If none has it, a question for an ollaya model raises EngineNotFound with the install command. ollaya keeps its models in its own store, OLLAYA_MODELS (default ~/.ollaya/models), not in the Hugging Face cache; set that variable if your home volume has no room.
ollaya is young and moves quickly. Its first release, v0.1.0, came out on 2026-09-23, and v0.7.5 followed on 2026-09-28, the fifteenth release in five days.1 Two of those releases matter for ornotto on a Mac. v0.7.0 added llama.cpp for GGUF models, and v0.7.1 added GPU inference on Apple silicon through MLX, on macOS 14 or later. Which device a model runs on is ollaya's choice, steered by its own settings such as OLLAYA_DEVICE, which ornotto passes through unchanged (engine lifecycle). The ollaya rows in this book were measured with the ollaya release installed at the time of the second benchmark round; a later release can change their speed.
Models download from Hugging Face on first use, through huggingface_hub, into the ordinary Hugging Face cache. Set HF_HUB_CACHE (or HF_HOME) if your home volume has no room: decider-0.8b needs 0.81 GB, and Qwen3.5-4B-Hmm needs 2.7 GB.
Additional System One models¶
Current main registers the fifteen repositories below. Each usable alias selects its compatible readout rather than routing every checkpoint through a generic letter slot. Weight revisions are pinned in the registry; CoreAI's external catalog controls its own revision.
| Alias | Source | Runtime or availability |
|---|---|---|
rune-systemone-q3 |
owao/surogate-rune-26b-a4b-systemone | llama |
raz-nli-xsmall-openjev |
RazvanManolache/raz-systemone-nli-xsmall-openjev | raz-nli |
raz-nli-base |
RazvanManolache/raz-systemone-nli-base | raz-nli |
system-one-scorer-onnx |
developerjeremylive/system-one-qwen3.5-4b-scorer-ONNX-etheroi | onnx-scorer |
oomu-systemone-0.6b |
oomu/OOMU-SystemOne-0.6B-GGUF | bosun-gguf |
systemone-lite-0.5b |
dwidlee/systemone-lite-0.5b | systemone-lite |
system-one-gemma-coreml |
FluidInference/system-one-gemma-coreml | Unavailable: Source-only conversion toolkit: no weights, adapter or Core ML package is published. The upstream Gemma base is gated. |
system-one-scorer-coreai |
mlboydaisuke/system-one-qwen3.5-4b-scorer-CoreAI | coreai |
system-one-minicpm5-2b-q8 |
mpuig/system-one-minicpm5-2b-q8 | system-one-mlx |
system-one-qwen3-0.6b |
mpuig/system-one-qwen3-0.6b | system-one-mlx |
nev-lite-systemone |
shreyanbr/nev-lite-systemone | Unavailable: The model card requires its custom System One runtime and calibration. The linked GitHub runtime repository returns HTTP 404 (2026-10-03); raw NLI is not an equivalent readout. |
system-one-270m |
kaivoss/system-one-270m | transformers-slot |
system-one-gold |
shreyanbr/system-one-gold | transformers-nli |
system-one-distilled |
shreyanbr/system-one-distilled | transformers-nli |
system-one-zeroshot |
shreyanbr/system-one-zeroshot | transformers-nli |
From a current main checkout, install only the runtime you need in the environment running ornotto. Transformers and ONNX adapters require Python 3.11+:
uv pip install -e ".[transformers]" # Raz NLI, Gemma letter slot, Shreyan NLI
uv pip install -e ".[onnx]" # ONNX Q4 scalar scorer; use gpu=False
uv pip install -e ".[bosun]" # OOMU; build/install a Metal-enabled wheel on Mac
uv pip install 'systemone-lite @ git+https://github.com/fritzprix/systemone-lite.git@fc6fbe3e9a3976d5c2976589f0f7176cfdcfb110'
uv pip install 'system-one @ git+https://github.com/mpuig/system-one.git@ed076ed47d5e09187b66697982f6aa65263387dc'
The Lite runtime also needs the Transformers extra. The mpuig runtime needs its MLX dependencies and Apple silicon; it has no CPU backend. Its per-primitive temperatures are bound to an exact model identity. ornotto presents exactly the calibration-listed backbone files and preserves the upstream hash check. The Qwen adapter downloads its separately pinned base model.
For Rune, install a current llama.cpp server with System One support, and set ORNOTTO_LLAMA_BIN if it is outside PATH. The new v3 Q3 alias uses the embedded native profile and temperature 2; the historical rune-26b-a4b q3 / slot (llama-server) measurement remains a separate readout and file revision. Its result is not evidence for the v3 native endpoint. The v3 command and protocol adapter are implemented, but a fresh native run was deferred after the CoreAI memory incident.
For CoreAI, install the external CoreAIKit systemone CLI. It requires macOS 27 and a Mac GPU. ornotto looks for systemone on PATH, in ~/.local/bin, or at ORNOTTO_COREAI_BIN, and launches serve --model system-one-scorer-4b. Run systemone models to inspect the catalog revision. This integration remains experimental: CLI 0.7.3 grew to 29.1 GB RSS and tripped the benchmark host’s swap watchdog during startup; it has no completed inference result. CoreAI downloads through its own cache, not HF_HUB_CACHE; its native row limit is 384 tokens and its runtime may truncate longer inputs. The ONNX adapter rejects overlong rows instead.
from ornotto import Decider, shutdown
try:
decider = Decider("system-one-minicpm5-2b-q8")
answer = decider.choose("Please explain kerning", {
"docs": "explain how the application works",
"code": "write a program",
})
print(answer.value, answer.probabilities)
finally:
shutdown() # release the model before loading another
All usable aliases support choice, yes/no and score, including async and typed consumers. gpu=True explicitly selects MLX, Metal or MPS; unsupported placement fails rather than silently selecting CPU. The ONNX adapter is CPU only. Raz uses the author's three-way entailment and contradiction recipe; its score is the modal level. The Gemma 270M adapter follows the card's raw letter-logit example (temperature 1), with at most 26 choices. Lite aliases caller keys to 26 distinct letter symbols and uses its upstream runtime without a prefix cache. OOMU uses Bosun's native stable decision tokens and prompt compiler, not a generic llama-server profile.
Gold, Distilled and ZeroShot use the model cards' standard Transformers zero-shot NLI path: raw entailment logits across options, the default This example is {}. hypothesis, and expected-level score aggregation. Their author's custom runtime URL returns HTTP 404; its calibration.json is not applied. These measurements are labelled NLI entailment logits, separate from the unavailable calibrated System One runtime. Nev Lite uses a different custom embedding/MLP architecture, so this fallback cannot implement it faithfully.
The FluidInference repository publishes a conversion toolkit without a trained adapter, weights or Core ML package. Nev Lite's required runtime is unavailable. Both aliases fail with an explicit reason before downloading or starting a process. No benchmark score is assigned to unavailable models. Temperature application on the remaining models does not establish calibration on your task; their answers keep calibrated=False.
Clef and CLM quantizations¶
Current main registers fourteen variants from seven requested repositories:
| Aliases | Quantization | Source | Readout |
|---|---|---|---|
clef-q3, clef-q4, clef-q5 |
Q3_K_M, Q4_K_M, Q5_K_M | bartowski/Cloudflare_clef-GGUF | Joint schema, llama.cpp |
clef-mlx4 |
MLX 4-bit | mlx-community/clef-4bit | Author MLX joint head |
clm-8b-q4-km, clm-8b-q5-km, clm-8b-q6-k |
Q4_K_M, Q5_K_M, Q6_K | czl/CLM-v0.1-8B-GGUF | Last-token encoder + reference heads |
clm-8b-mlx6 |
MLX 6-bit | czl/CLM-v0.1-8B-MLX-6bit | Author encoder + reference heads |
clef-flash-q3, clef-flash-q4, clef-flash-q5, clef-flash-q6 |
Q3_K_M, Q4_K_M, Q5_K_M, Q6_K | bartowski/Cloudflare_clef-flash-GGUF | Joint schema, llama.cpp |
clef-flash-mlx8 |
MLX 8-bit | TrevorJS/clef-flash-mlx-8bit | Author MLX joint head |
clef-flash-mlx4 |
MLX 4-bit | TrevorJS/clef-flash-mlx-4bit | Author MLX joint head |
Use Python 3.11+ and install the required runtime from the checkout:
uv pip install -e '.[clef]' # attach native head to a Clef GGUF
uv pip install -e '.[clef-mlx]' # Apple silicon MLX, including MLX-VLM
uv pip install -e '.[clm]' # CLM GGUF embeddings, Torch heads
uv pip install --no-deps contrastive-lm==0.1.0
# CLM MLX also needs the mlx-lm and Transformers dependencies:
uv pip install -e '.[clef-mlx,transformers]'
The CLM package is installed with --no-deps because its server dependency list includes vLLM; this adapter uses local embeddings instead. The clm extra supplies NumPy, Torch and requests. For GPU GGUF execution, install a Metal- or CUDA-enabled llama-cpp-python build. Clef GGUF uses a separate llama.cpp b11371 server, selected by ORNOTTO_LLAMA_BIN. It is not the llama.cpp bundled inside the ornotto wheel.
The pinned bartowski GGUFs contain a Qwen backbone without Clef's decision tensors or metadata. On first use, ornotto downloads only the matching Cloudflare Clef or Clef-Flash head and creates a head-hash-keyed -clef-native-….gguf beside the immutable source file. It copies quantized backbone tensors byte for byte, adds the FP32 joint head, native schema template and decision metadata using llama.cpp's pinned conversion layout, and publishes the derived file atomically. Allow disk space for both the original and derived GGUF; the derivation preserves the original download. The native head adds storage and resident memory beyond the quantizer's listed backbone size.
Clef answers all questions jointly in one prefill and supports text states in this adapter. The managed server reserves a 4096-token context and equally sized batch/microbatch. MLX adapters reject an overlong complete schema before inference; no state is silently sliced. CLM uses the original context-plus-question rendering, last-token pooling, normalized state/action projections and native temperature recipe from contrastive-lm==0.1.0. Its small FP32 projection heads run on CPU, with the encoder on Metal or MLX. CLM accepts up to 2048 tokens per encoded text, rejecting longer inputs. Its reference head has a separately pinned revision. Plain K_M/K GGUF variants have distinct benchmark identities from earlier outq2 files.
All aliases use the same choice, yes/no, score, async and typed API. Call shutdown() between models. Calibration on your task is unverified (calibrated=False). Registered support and downloaded files do not imply a completed accuracy measurement; only successful full runs enter the benchmark.
The October 3 campaign completed nine of these configurations. Clef-Flash Q3 scores 62/64 translated/direct; Q4, Q5 and Q6 each score 63/64; MLX 4-bit scores 63/63. CLM Q4/Q5/Q6 and MLX 6-bit score 36, 38, 38 and 39 translated, respectively. Their low-scoring rows remain, but their downloaded weights were pruned. Clef Q3/Q4/Q5, Clef MLX 4-bit and Clef-Flash MLX 8-bit have pinned integrations and completed downloads; their benchmarks were deferred by the available-memory preflight. No accuracy is assigned to them. The 48 GiB host had 22 GiB available; these configurations required 30, 35, 40, 33 and 24 GiB, respectively, under the unchanged reserve rule.
Native encoders¶
Current main supports aac6fef/laya-multilingual-mlx
and mccoysc/xDecision.
Install ornotto from the checkout with uv pip install -e '.[laya-mlx]'
on Apple silicon macOS, Python 3.11+. xDecision is a separate pinned source install:
uv pip install 'xdecision[apple] @ git+https://github.com/xnetsc/xDecision.git@4082689a093393358534fda26a945699080eb572'
For xDecision CPU, omit [apple]. MLX GPU requires Apple silicon. The runtimes
are not bundled and must be installed in the same Python environment as ornotto.
encoder = ornotto.Decider("laya-multilingual-mlx")
answer = encoder.choose("Rename glyphs with a Python script", ["python", "docs"])
ornotto.shutdown() # stop the first model before loading another
encoder = ornotto.Decider("xdecision-q8", gpu=False)
answer = encoder.check("Rename glyphs with a Python script", "Does the user want code?")
ornotto.shutdown()
Registered names download the native checkpoint or custom GGUF into the Hugging
Face cache. A local checkpoint directory works with engine="laya-mlx";
a local xDecision GGUF works with engine="xdecision". These formats cannot be
sent to pcdServer or dohnuts. Native metadata/head overrides are rejected.
The default device is GPU; gpu=False selects native MLX CPU for Laya and
PyTorch CPU for xDecision. MLX expands xDecision Q8_0 to FP16 at load, so the
smaller file does not imply proportionally smaller memory or quantized matmul.
The usual sync/async calls, typed extraction and pydantic-ai bridge use the same
questions and answers. Native probability, confidence, action and usage fields
are preserved in the raw response. calibrated remains false because task
calibration has not been established. Missing instructions get a default for
the question kind; explicit instructions are passed through. Reported state
truncation raises DecisionError; a runtime that omits truncation metadata
cannot establish that every input token was retained. Keep inputs within its
context (1,024 tokens for this Laya checkpoint), including questions/options.
ornotto starts one serial loopback HTTP process per model/engine/device and
stops it through the existing lifecycle.
Native Core AI: Clef-Flash and PII¶
The Clef-Flash conversion
uses its native Core AI decoder, joint-schema head and FP16 lexical table.
clef-flash-coreai-fp16 and clef-flash-coreai-int8mix download immutable
revision 220ac149ed1136465f7d9dac93aaf0dfc55d6b73. They accept text and JSON
states through Decider; the adapter does not expose the conversion's image
input. Questions share one prefill and the native head. Full inputs that exceed
4096 tokens are rejected. These optional engines require Apple silicon,
macOS 27, an Xcode toolchain with CoreAI, and the separately built Swift bridge:
git clone --recursive https://github.com/fontlaborg/ornotto
cd ornotto
python3 runtimes/coreai/build.py
export ORNOTTO_COREAI_BRIDGE_BIN="$PWD/runtimes/coreai/.build/release/ornotto-coreai"
from ornotto import Decider, CoreAIExtractor, shutdown
try:
decider = Decider("clef-flash-coreai-int8mix")
answer = decider.choose("The payment failed", ["billing", "technical"])
finally:
shutdown()
with CoreAIExtractor() as extractor:
result = extractor.extract("Email alice@example.org", ["email"])
print(result["entities"])
print(result["redacted"])
CoreAIExtractor runs the GLiNER2 PII conversion
at revision 74fb5c19e7eba2d4ebad95f90eeb6d996432575d. It returns native
entity labels, matched strings, confidence scores, UTF-16 span offsets and
redacted text. Use await extractor.aextract(...) for asynchronous calls.
Labels are supplied at each call: 1–16 unique nonempty strings, threshold
0–1 (default 0.5). Its static graph accepts at most 96 text words and 256
combined schema/text tokens. The pinned runtime patch rejects either overflow
before inference, rather than silently dropping trailing words. Entity spans
are limited to eight words. Native confidence scores are not calibrated
routing probabilities. Closing the context stops the resident native process.
Downloads use the configured Hugging Face cache. Set HF_HUB_CACHE to your
model volume before first use; these bundles are not included in Python wheels.
The bridge serializes requests and loads one selected native model per process.
Chapter 6 records the PII measurement
and Clef's memory refusals.
Decider¶
A Decider is one model on one engine. Creating one resolves the model (downloading it if needed) but does not start the engine. The engine starts on the first question.
import ornotto
fast = ornotto.Decider("decider-0.8b") # dohnuts, the default engine for this model
same = ornotto.Decider("decider-0.8b", engine="pcd") # the same weights on pcdServer
mine = ornotto.Decider("~/models/my-model.gguf") # any chat GGUF, on pcdServer
hf = ornotto.Decider("hf:bartowski/Qwen_Qwen3.5-2B-GGUF/Qwen_Qwen3.5-2B-Q4_K_M.gguf")
up = ornotto.Decider("decider", url="http://127.0.0.1:8298") # an engine you started yourself
laya = ornotto.Decider("ollaya-laya-multilingual") # a registered ollaya model
tag = ornotto.Decider("ollaya:kev:4b") # any ollaya tag; same as Decider("kev:4b", engine="ollaya")
| Argument | Default | Meaning |
|---|---|---|
model |
"decider-0.8b" |
A registered name, a local .gguf path (~ expands), hf:owner/repo/file.gguf, or an ollaya tag as ollaya:<tag>. |
engine |
first engine the model supports | "dohnuts", "pcd", "ollaya", "laya-mlx" or "xdecision". With "ollaya", a plain tag such as kev:4b also works. Asking a model for an engine it cannot run raises ValueError. |
url |
None |
Talk to a running engine at this base URL instead of starting one. engine then defaults to "dohnuts", and model is only a label. |
gpu |
True on macOS, False elsewhere |
Offload all layers to the GPU. The bundled macOS build has Metal; the Linux and Windows builds are CPU only. Affects both engines. With False, pcdServer disables GPU weights, computation and KV offload. |
metadata |
None |
Explicit dohnuts profile JSON, overriding the registered profile too. Without it, an unregistered GGUF runs on pcdServer only. |
head |
None |
Explicit scorer head for kev or Dohnuts, overriding the registered head too. |
timeout |
60.0 |
Seconds to wait for one answer. |
A Decider has four methods, plus an async one. Each takes the state you ask about: a string, or any JSON value (a dict, a list, numbers).
| Method | Asks | Returns |
|---|---|---|
decide(state, questions) |
named questions, in one call | Decision |
choose(state, options, question=None) |
pick one of options |
Answer |
check(state, question, *, yes=None, no=None) |
yes or no | Answer |
rate(state, question, levels) |
a level on a rubric | Answer |
adecide(state, questions) |
decide, for asyncio |
Decision |
options is a list of names or a mapping of name to description. levels[i] describes level i. yes and no optionally say what counts as each answer.
The url property returns the engine's base URL, starting the engine if needed. Use it when you want to call the engine's HTTP API directly.
Remote OpenRouter models¶
Use current main for remote support (source build).
Set OPENROUTER_API_KEY, then use an exact model ID:
remote = ornotto.Decider("liquid/d1")
answer = remote.choose("Build a kern feature", ["docs", "python", "fea"])
All nine IDs are listed by ornotto models and ornotto.OPENROUTER_MODELS:
liquid/d1, togethercomputer/tev1-4b-experimental, inception/mercury-decide:free,
upstage/solar-decide, respan/span-01, respan/span-01-lite,
respan/span-01-lite:free, jaredpalmer/kev-4b, typesafe/jev-1.13.
The three Respan variants support only yes/no (noul) questions. Use check()
or decide() with yes_no(); choice and score are rejected locally before
sending a request. OPENROUTER_KINDS and ornotto models expose the supported
question kinds. A pydantic-ai schema for Respan must contain boolean fields.
Other decision IDs use Decider("owner/model", engine="openrouter") or the
openrouter:owner/model prefix. An explicit api_key=... overrides the environment.
CLI example: ornotto choose "Build a kern feature" docs python fea --model=liquid/d1.
adecide, extract, ornotto.use and pydantic_ai.model work with these models.
Nothing downloads or starts locally. gpu, metadata, head, pull and serve
are local settings/operations and are rejected for remote models. Remote calls
use the System One API
with a timeout and no automatic retry. A returned Decision retains usage
(including reported USD cost), resolved_model, provider and response_id.
calibrated=False means the provider's temperature recipe is unrecorded.
Remote timings include network latency; benchmark device R means CPU/GPU undisclosed.
Availability and access are controlled by OpenRouter, including :free models.
Module functions¶
The module functions use one shared Decider, created on first use from the arguments of the last ornotto.use() call.
| Function | Same as |
|---|---|
ornotto.decide(state, questions) |
Decider.decide |
ornotto.choose(state, options, question=None) |
Decider.choose |
ornotto.check(state, question, *, yes=None, no=None) |
Decider.check |
ornotto.rate(state, question, levels) |
Decider.rate |
ornotto.use(model="decider-0.8b", *, engine=None, **kwargs) |
sets the shared Decider's arguments; the next call creates it again |
ornotto.default() |
returns the shared Decider |
ornotto.shutdown() |
stops every engine this process started |
ornotto.use("qwen3.5-4b-hmm") # every module function now runs Qwen3.5-4B-Hmm on pcdServer
ornotto.choose("Kern the class of round lowercase letters against the period",
["kern pair", "kern class", "class definition"])
Questions¶
decide takes a mapping from question name to question. The name you choose comes back as the key of the answer. Build questions with three functions:
ornotto.choice("What does the user want?", {"docs": "an explanation from the manual",
"python": "a script", "fea": "feature code"})
ornotto.yes_no("Does the user want code they can run?", yes="runnable code", no="an explanation")
ornotto.score("How urgent is it?", ["can wait", "this week", "today"])
| Builder | System One type | Rules |
|---|---|---|
choice(instructions, options) |
choice |
at least two options; options is a list of names or a name → description mapping |
yes_no(instructions, *, yes=None, no=None) |
noul |
yes and no are optional descriptions |
score(instructions, levels) |
score |
at least two levels, in order from level 0 |
You can also pass a System One question as a plain dict, as the jev API and dohnuts accept it: {"type": "choice", "instructions": "...", "criteria": {...}}. question is accepted for instructions, options for criteria, and bool for noul. A score whose criteria are keyed by numbers ({"0": ..., "1": ...}) is sorted numerically. Instructions and criteria may be JSON values; ornotto turns them into text.
Limits per request¶
dohnuts takes at most 64 questions per request (ornotto starts it with --max-questions 64; the server default is 8), ollaya 256, and pcdServer at most 63 fields. If you ask more, your Decider splits them into several requests and merges the answers into one Decision. For pcdServer, ornotto also checks the 64 KiB state limit before it sends anything and clips each question's text, option descriptions included, to 1,024 bytes. Chapter 2 lists every limit of both engines.
Answers¶
An Answer is one answer. Its fields mean slightly different things for the three kinds of question you can ask.
| Field | choice | yes/no | score |
|---|---|---|---|
kind |
"choice" |
"noul" |
"score" |
value |
the option name | True when p(yes) ≥ 0.5 |
the expected level, a float |
probabilities |
per option | {"false": p, "true": p} |
per level, keyed "0", "1", … |
probability |
of the chosen option | of yes | of the most likely level |
confidence |
of the chosen option | max(p(yes), p(no)) | of the most likely level |
level |
raises AttributeError |
raises AttributeError |
the most likely level, an int |
raw |
the engine's answer as a dict | same | same |
bool(answer) is bool(answer.value), so if ornotto.check(text, "Is it spam?"): reads as it should.
calibrated identifies the readout: True for dohnuts and ollaya, False for pcdServer and the native encoders's plain token softmax. Native profiles apply the temperature in their metadata. That can be a fitted temperature, as in decider, or 1.0, as in the experimental Tev1 and ThisThat profiles. The flag is not evidence that a probability of 0.8 will be right 80 percent of the time on your task. Check the profile and your own labelled examples before using a confidence threshold. Chapter 9 explains that check.
raw keeps everything the engine sent, including fields ornotto does not model. From dohnuts that includes native.confidence (the top probability) and native.certainty (1 − H/ln K); for isolated scores it adds native.level_fit and native.fit_mass.
A Decision holds all answers to one request. It is a read-only mapping from question name to Answer, and it also answers attribute access:
d = ornotto.decide("Rename every .sc glyph to .smcp in all open fonts", {
"task": ornotto.choice("What does the user want?", ["docs", "python", "fea"]),
"code": ornotto.yes_no("Does the user want code they can run?"),
})
d.task.value, d["code"].probability, d.to_dict()
| Field | Meaning |
|---|---|
answers |
the dict of answers |
model |
the model name |
engine |
"dohnuts", "pcd", "ollaya", "laya-mlx" or "xdecision" |
usage |
numeric usage fields summed over the requests, for example input_tokens from dohnuts or elapsed_ms from pcdServer |
ms |
client wall-clock time for the whole call, engine start excluded |
to_dict() |
just the values, by question name |
Async¶
adecide is decide for asyncio. It uses httpx.AsyncClient, so many decisions can be in flight at once. The first call may block briefly while the engine starts: starting a process and waiting for it to load a model is not asynchronous.
import asyncio
async def main():
d = ornotto.Decider("decider-0.8b")
texts = ["How do I open the Glyph window?", "Build a kern feature for A V"]
q = {"task": ornotto.choice("What does the user want?", ["docs", "python", "fea"])}
return await asyncio.gather(*(d.adecide(t, q) for t in texts))
asyncio.run(main())
ornotto.aextract is the async form of extract (chapter 11).
Engine lifecycle¶
The engine is a local HTTP server that ornotto runs as a child process. You never start it by hand unless you want to.
- The first question asks
Decider.url, which callsServer.shared(engine, model, gpu). - ornotto binds a socket to
127.0.0.1on port 0 to get a free port, and starts the engine on that port, loopback only. - The engine's stdout and stderr go to a log file in the temp directory, named
ornotto-<engine>-<port>.log. A file never fills up and blocks the engine the way a full pipe would. - ornotto polls
/healthevery 0.2 s until the model is loaded, for up to 180 s. If the process exits first, the error includes the last 2,000 characters of its log. - At interpreter exit, an
atexithandler stops every engine: terminate, wait 10 s, then kill.
An ollaya engine is a private ollaya serve on its own loopback port, so it never touches an ollaya daemon you run yourself on ollaya's default port. ornotto starts it holding one model and keeping it loaded, waits until / answers, pulls the tag if the store lacks it, and loads the model through /api/decide before the first question, allowing up to 600 s for that load. At exit it stops the server with ollaya stop. Your OLLAYA_* settings, such as OLLAYA_MODELS and OLLAYA_DEVICE, pass through, except that ornotto sets the address, the one-model limit and the keep-alive itself and leaves out OLLAYA_API_KEY and OLLAYA_LOG_DIR.
Every Decider in the process that names the same engine, model files and GPU setting shares one engine process. Opening Decider("decider-0.8b") twice costs one load. If the process died, the next question starts a new one.
Memory
Each engine holds its model in memory for as long as it runs. Two Deciders on different models are two processes, both resident. ornotto.shutdown() stops them all at once.
Registered models¶
Build current main for the October registry and engine updates (source build). ornotto.MODELS maps names to ModelSpec records. ornotto models prints the same list, with sizes rounded to one decimal and licence ids in lowercase.
| Name | Engines | Family | GB | Licence | Source |
|---|---|---|---|---|---|
decider-0.8b |
dohnuts, pcd | dedicated | 0.81 | Apache-2.0 | DreamBlooms/decider-0.8b-GGUF |
decider-2b |
dohnuts, pcd | dedicated | 2.0 | Apache-2.0 | DreamBlooms/decider-2b-GGUF |
kev-0.8b |
dohnuts | dedicated | 0.81 | Apache-2.0 | DreamBlooms/kev-0.8b-GGUF |
tev1-0.8b |
dohnuts | dedicated | 0.81 | unresolved | DreamBlooms/Tev1-0.8B-experimental-GGUF |
jet-4b |
dohnuts | dedicated | 4.48 | Apache-2.0 | DreamBlooms/jet-GGUF |
this-that-1.2 |
dohnuts | dedicated | 1.27 | MIT | mradermacher/this-that-model-1.2-GGUF |
jpt-4b |
dohnuts, pcd | dedicated | 2.71 | CC-BY-NC-4.0 | prithivMLmods/jpt-4b-GGUF |
jpt-9b |
dohnuts, pcd | dedicated | 5.63 | CC-BY-NC-4.0 | prithivMLmods/jpt-9b-GGUF |
rune-26b-a4b |
pcd | dedicated | 13.29 | Apache-2.0 | mradermacher/rune-26b-a4b-GGUF |
dohnuts-0.8b |
dohnuts | dedicated | 0.81 | CC-BY-NC-SA-4.0 | DreamBlooms/Dohnuts-0.1.0-0.8B-GGUF |
qwen3.5-0.8b |
pcd | vanilla | 0.83 | Apache-2.0 | ggml-org/Qwen3.5-0.8B-GGUF |
qwen3.5-2b |
pcd | vanilla | 1.4 | Apache-2.0 | bartowski/Qwen_Qwen3.5-2B-GGUF |
qwen3.5-4b |
pcd | vanilla | 3.0 | Apache-2.0 | bartowski/Qwen_Qwen3.5-4B-GGUF |
qwen3.5-4b-hmm |
pcd | fine-tuned | 2.7 | Apache-2.0 | n4ze3m/Qwen3.5-4B-Hmm |
jevk5-4b |
pcd | fine-tuned | 2.7 | Apache-2.0 | alibiserikbay/JevK5-GGUF |
openjev-35b-a3b |
pcd | fine-tuned | 21.2 | Apache-2.0 | apus-ailab/APUS-OpenJev-v1-35B-A3B-GGUF |
ollaya-kev-0.8b |
ollaya | dedicated | 1.8 | Apache-2.0 | ollaya kev:0.8b |
ollaya-decider-0.8b |
ollaya | dedicated | 1.5 | Apache-2.0 | ollaya decider:0.8b |
ollaya-laya-en |
ollaya | dedicated | 0.9 | Apache-2.0 | ollaya laya:en |
ollaya-laya-multilingual |
ollaya | dedicated | 0.7 | Apache-2.0 | ollaya laya:multilingual |
ollaya-nli-modernbert-large |
ollaya | dedicated | 0.8 | Apache-2.0 | ollaya nli:modernbert-large |
ollaya-gliclass-large |
ollaya | dedicated | 1.8 | Apache-2.0 | ollaya gliclass:large |
ollaya-von-1.1 |
ollaya | dedicated | 1.6 | Apache-2.0 | ollaya von:1.1 |
ollaya-decision-eos |
ollaya | dedicated | 1.5 | Apache-2.0 | ollaya decision:eos |
ollaya-winnow-12b |
ollaya | dedicated | 12.7 | Apache-2.0 | ollaya winnow:12b |
ollaya-clm-8b |
ollaya | dedicated | 16.5 | Apache-2.0 | ollaya clm:8b |
The registry files jevk5-4b and openjev-35b-a3b as fine-tuned pcdServer models; the benchmark tables count them as dedicated, because each has a trained letter readout of its own (chapter 4). The dedicated GGUF models use Q8_0 except JPT and ThisThat (Q4_K_M), and Rune (Q3_K_M); the Qwen chat models, JevK5 and APUS-OpenJev are Q4_K_M, except the 0.8B, which is Q8_0. openjev-35b-a3b needs about 23 GB of memory, so run it with nothing else loaded. An ollaya model is its registry tag, and its size is ollaya's download; ollaya picks the runtime and precision for each tag. On the router benchmark, ollaya-winnow-12b scored 64/67 and ollaya-decider-0.8b 61/67, while ollaya-clm-8b, ollaya-gliclass-large and ollaya-nli-modernbert-large scored 39, 26 and 20 (chapter 6). Every other tag in ollaya's registry works as ollaya:<tag>. For dohnuts, a dedicated model downloads its profile JSON or uses a bundled one (JPT 4B, JPT 9B and ThisThat), and kev and Dohnuts download a scorer head. Dohnuts and JPT weights are for non-commercial use only. The JPT Q8 benchmark files are local conversions of pinned BF16 weights, not upstream downloads. Explicit metadata= and head= overrides are honored for registered models too. Chapter 4 explains the three families, and chapter 12 which to pick.
ornotto.DEFAULT_MODEL is "decider-0.8b".
ornotto and ollaya¶
ollaya is the project nearest to ornotto, and it is also one of ornotto's engines. Its site describes it as "An independent open-source project, not affiliated with Ollama or TypeSafe".2 It is one Rust binary that runs a daemon, pulls decision models from its own registry, and answers them over TypeSafe's /v1/systemone protocol. The site states that "The official TypeSafe Python SDK 0.7.1 works unchanged against a local server." If all you need is a local System One endpoint for code that already talks to jev, ollaya on its own may be enough.
ornotto solves a neighbouring problem. It is a Python package that puts several engines behind one Python API, and ollaya is one of them:
| ornotto | ollaya on its own | |
|---|---|---|
| What you install | a Python wheel with dohnuts and pcdServer inside; ollaya optional | one binary |
| How you call it | Python: Decider, decide, choose, extract, @decision, pydantic-ai |
HTTP: /v1/systemone, plus its own endpoints and an MCP server |
| Models | Hugging Face GGUF files by name, path or hf: reference, plus any ollaya tag |
ollaya's registry |
| Engines | dohnuts, pcdServer, ollaya | ollaya's runtimes: ONNX Runtime, llama.cpp and MLX |
| Typed Python | pydantic models, typed functions, pydantic-ai agents (chapter 11) | through TypeSafe's SDK or pydantic-ai's TypeSafeModel |
| Engine lifecycle | started per model on a free loopback port, stopped at exit | a daemon you start and stop |
The practical difference is what happens when you change your mind about a model. With ollaya alone, you choose among the models its registry offers. With ornotto, a vanilla chat GGUF, a dedicated model on dohnuts and an ollaya tag are three Decider lines in the same script, and the benchmark in chapter 6 is the reason you might want all three: the fastest, the most accurate and the easiest-to-install model are not the same model.
The overlap is also real. The laya encoders, winnow, von, GLiClass, the ModernBERT NLI model, Decision 1.0 Eos run in ornotto only through ollaya (registered models). ornotto adds no readout of its own for them. It starts a private ollaya server, pulls the tag, and reads ollaya's answer, calibration included.
ollaya's releases add features that ornotto does not wrap. v0.4.0 added an MCP server, v0.6.0 a --preset agent that decides whether an agent's action should run, ask or be blocked, and v0.7.5 image input through decider:2b-vision.1 None of them was part of our benchmark. From v0.7.4 ollaya's release notes recommend winnow:e4b for general use; ornotto registers winnow:12b, which is the one we measured, and winnow:e4b runs as Decider("winnow:e4b", engine="ollaya") without a registry entry.
How it looked from the outside
ollaya's Show HN thread, "Ollaya – Ollama for open-source, Jev-style decision models", reached 613 points on 2026-09-25.3 In the comments, one reader summed up how long open source had taken to copy TypeSafe's idea: "what, like 2 weeks?" TypeSafe had launched jev on 2026-09-15; ollaya's first release is dated 2026-09-23.
What changed in September 2026¶
The package's API did not change during the month, but three things around it did:
- ollaya went from its first release to v0.7.5 and gained a GPU path on Apple silicon (above).
- dohnuts upstream merged our prefix-cache pull request on 2026-09-24 and added a cross-call cache of its own. The wheels still build dohnuts from our fork's branch; chapter 12 describes that state and chapter 8 the cache.
- pydantic-ai and typesafe-sdk released changes that a fresh install of the
pydantic-aiextra can pick up. ornotto's lock pins the versions it was tested with (chapter 11).
Errors¶
| Exception | Raised when |
|---|---|
EngineNotFound |
the engine executable is not in the environment variable, the wheel or PATH (for ollaya: the variable, PATH or ~/.local/bin) |
DecisionError |
the engine answered with an HTTP status other than 200; the message carries the status and the first 500 characters of the body. ollaya's 422 STATE_TRUNCATED says that the state is longer than the model's context; ollaya-laya-en reads only 512 tokens |
ValueError |
a question has fewer than two options or levels; a model does not run on the requested engine; an unregistered GGUF asks for dohnuts without metadata; the state for pcdServer exceeds 64 KiB |
FileNotFoundError |
the model is neither a registered name nor a file; the message lists the registered names |
RuntimeError, TimeoutError |
the engine exited while loading, or did not load within 180 s; the message names the log file |
httpx.HTTPError |
the connection to the engine failed |
Command line¶
The ornotto command wraps the same API. Each command prints its result, as JSON where it returns an answer.
| Command | Does |
|---|---|
ornotto models |
list the registered models |
ornotto pull NAME |
download a model and its metadata and print the GGUF path; for an ollaya model, pull the tag into OLLAYA_MODELS |
ornotto choose STATE OPTION... [--model=] [--engine=] [--question=] |
pick one option |
ornotto check STATE QUESTION [--model=] [--engine=] |
answer yes or no |
ornotto serve [NAME] [--engine=] |
start an engine, print its URL, run until Ctrl+C |
ornotto version |
print the package version |
ornotto choose "Build a kern feature for A V W T" docs python fea
ornotto serve qwen3.5-4b-hmm --engine=pcd
ornotto pull ollaya-kev-0.8b
ornotto choose "Build a kern feature for A V W T" docs python fea --model=kev:4b --engine=ollaya
ornotto serve is the easy way to get a server that another process can reach. Pass its URL to Decider(url=...), or point any System One client at it if the engine is dohnuts or ollaya.
-
ollaya, "Releases", read 2026-09-30. https://github.com/ollaya-dev/ollaya/releases ↩↩
-
ollaya, "ollaya", read 2026-09-30. https://ollaya.dev ↩
-
Hacker News, "Ollaya – Ollama for open-source, Jev-style decision models", 2026-09-25. https://news.ycombinator.com/item?id=49848269 ↩