Skip to content

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:

  1. The path in ORNOTTO_DOHNUTS_BIN or ORNOTTO_PCD_BIN.
  2. The copy bundled in the wheel, under ornotto/_bin/.
  3. dohnuts-cli or pcd_server on your PATH.

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:

curl -fsSL https://ollaya.dev/install.sh | OLLAYA_INSTALL_DIR=$HOME/.local sh

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.

  1. The first question asks Decider.url, which calls Server.shared(engine, model, gpu).
  2. ornotto binds a socket to 127.0.0.1 on port 0 to get a free port, and starts the engine on that port, loopback only.
  3. 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.
  4. ornotto polls /health every 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.
  5. At interpreter exit, an atexit handler 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-ai extra 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.


  1. ollaya, "Releases", read 2026-09-30. https://github.com/ollaya-dev/ollaya/releases ↩↩

  2. ollaya, "ollaya", read 2026-09-30. https://ollaya.dev ↩

  3. Hacker News, "Ollaya – Ollama for open-source, Jev-style decision models", 2026-09-25. https://news.ycombinator.com/item?id=49848269 ↩