ornotto
ornotto asks a local language model to decide, not to write. You describe a decision (pick one of these options, answer yes or no, place this on a rubric) and get back an answer with a probability for every alternative. Nothing is generated, so nothing has to be parsed, repaired or retried.
It runs two open-source C++ engines on llama.cpp and gives them one Python API:
- dohnuts (dohnuts.cpp) runs models trained for this job (decider, kev, Dohnuts) and reads their answer at a trained slot. Its probabilities are temperature-calibrated.
- pcdServer (pcdServer) runs any chat GGUF and scores only the tokens of the answers you allow. It needs no special model.
The book at fontlab.org/ornotto explains how the engines work and which model to pick, and has the benchmarks.
Install
uv add ornotto # or: pip install ornotto
uv add "ornotto[pydantic-ai]" # with the pydantic-ai integration
Wheels bundle both engines for macOS 14+ on Apple silicon (Metal) and for Linux x86_64 and aarch64 (manylinux_2_28, CPU only: no CUDA or Vulkan). Windows x64 wheels carry dohnuts only: pcdServer does not build on Windows yet. Elsewhere pip installs the Python code without engines: clone with --recursive and run ./build.sh, or put dohnuts-cli and pcd_server on your PATH (or point ORNOTTO_DOHNUTS_BIN and ORNOTTO_PCD_BIN at them).
Models download from Hugging Face on first use into the usual Hugging Face cache. HF_HUB_CACHE moves it.
Decide
import ornotto
ornotto.choose("Build a kern feature for A V W T", ["docs", "python", "fea"]).value
# 'fea'
answer = ornotto.check("Write me a script that renames glyphs", "Does the user want code?")
answer.value, answer.probability
# (True, 0.6)
Several questions about one state go in one call. The state can be text or any JSON value:
decision = ornotto.decide(
{"message": "Why does this pair look too tight?", "selection": ["A", "V"]},
{
"area": ornotto.choice("Which area is this about?", {
"kerning": "space between a specific pair of glyphs",
"sidebearings": "space built into each glyph",
}),
"urgent": ornotto.yes_no("Is the user blocked?"),
"skill": ornotto.score("How experienced is the user?", ["beginner", "intermediate", "expert"]),
},
)
decision.area.value, decision.area.probabilities, decision.skill.value
Each Answer has value, probabilities, probability, confidence and calibrated. calibrated is true for dohnuts, whose probabilities are temperature-scaled, and false for pcdServer, whose probabilities are a raw softmax over the allowed tokens. Compare confidences only between answers of the same kind.
Pick a model and engine
fast = ornotto.Decider("decider-0.8b") # dohnuts, the default
same_weights = ornotto.Decider("decider-0.8b", engine="pcd")
careful = ornotto.Decider("qwen3.5-4b-hmm") # a fine-tuned chat model; pcdServer only
mine = ornotto.Decider("~/models/my-model.gguf") # any chat GGUF, on pcdServer
remote = ornotto.Decider("decider", url="http://127.0.0.1:8298") # an engine you started yourself
ornotto models lists the registered models:
| name | engines | family | GB | licence |
|---|---|---|---|---|
decider-0.8b |
dohnuts, pcd | dedicated | 0.8 | apache-2.0 |
decider-2b |
dohnuts, pcd | dedicated | 2.0 | apache-2.0 |
kev-0.8b |
dohnuts | dedicated | 0.8 | apache-2.0 |
dohnuts-0.8b |
dohnuts | dedicated | 0.8 | cc-by-nc-sa-4.0 |
qwen3.5-0.8b, qwen3.5-2b, qwen3.5-4b |
pcd | vanilla | 0.8, 1.4, 3.0 | apache-2.0 |
qwen3.5-4b-hmm |
pcd | fine-tuned | 2.7 | apache-2.0 |
The engine starts on the first question, on a free loopback port, and stops when Python exits. Every Decider in a process that names the same model, engine and device shares one engine process. ornotto.shutdown() stops them all now.
Typed decisions
A pydantic model becomes one question per field: a Literal or Enum of strings is a choice, bool is yes or no, and a float with ge=0, le=1 is the probability of yes. The field description is the question.
from typing import Literal
from pydantic import BaseModel, Field
class Triage(BaseModel):
task: Literal["docs", "python", "fea"] = Field(description="What does the user want?")
wants_code: bool = Field(description="Does the user want code they can run?")
ornotto.extract(Triage, "Write a Python script that renames every .sc glyph")
# Triage(task='python', wants_code=True)
A typed function with only a docstring becomes a decision, in the style of magentic and promptic. Its docstring is the question, and several arguments become one JSON state:
@ornotto.decision
def route(request: str) -> Literal["docs", "python", "fea"]:
"""Which kind of help does this FontLab user want?"""
route("How do I open the Glyph window?") # 'docs'
ornotto.classify(text, labels) follows marvin's classify. These are idioms, not integrations: marvin 3 does not currently import against pydantic-ai 2.
pydantic-ai
pydantic-ai's TypeSafeModel turns an agent's output_type into System One questions, the protocol dohnuts speaks. ornotto.pydantic_ai.model() points it at a local engine. For pcdServer, an in-process transport translates each request.
from pydantic_ai import Agent
from ornotto.pydantic_ai import model
agent = Agent(model("decider-0.8b"), output_type=Triage, instructions="Classify a FontLab user's request.")
agent.run_sync("Build a kern feature for A V W T").output
Rubrics (int fields with a description per level), lists of options and nested models work as pydantic-ai documents them for TypeSafeModel.
Command line
ornotto models
ornotto pull decider-0.8b
ornotto choose "Build a kern feature" docs python fea
ornotto check "Rename all .sc glyphs" "Does the user want code?"
ornotto serve qwen3.5-4b-hmm --engine=pcd
Develop
git clone --recursive https://github.com/fontlaborg/ornotto
cd ornotto
./build.sh # lint, unit tests, sdist and a wheel with both engines compiled in
ENGINES=1 ./test.sh # also the real-engine tests and examples/, one engine at a time
./publish.sh # tag the next version, let CI build every wheel, upload with uv publish
The engines are git submodules in engines/. The version comes from git tags via hatch-vcs.
Licence
ornotto is Apache-2.0. The wheels contain dohnuts.cpp (Apache-2.0), pcdServer, llama.cpp, cpp-httplib and nlohmann/json (MIT). Their notices are in licenses/. Model weights keep their own licences, listed above.
Release files for ornotto 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ornotto-0.1.1.tar.gz | 34.0 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| ornotto-0.1.1-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| ornotto-0.1.1-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
| ornotto-0.1.1-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details |
| ornotto-0.1.1-py3-none-macosx_14_0_arm64.whl | Python 3 | none | macOS 14.0+ ARM64 | Details |
Total release size: 19.5 MB
Release files / ornotto-0.1.1.tar.gz
| Download URL | ornotto-0.1.1.tar.gz |
|---|---|
| Size | 34.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5429dc1169faace52dba51216a0e099b9bed5a2ead4603bfc130b14bf62e43e4
|
|
BLAKE2b-256 checksum How to use checksums |
b3c1a6eea0a0caea52ac4e86f9e987d3f99b8049b6c6903f8edad1a27b0c882d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ornotto-0.1.1-py3-none-win_amd64.whl
| Download URL | ornotto-0.1.1-py3-none-win_amd64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
f214002c23c848edb154f8f670d175abaa91f61bdee6657cb72500a4d28c42e7
|
|
BLAKE2b-256 checksum How to use checksums |
e928fd61de5beb05bdaaaec74358c1837f8c69e9c228190669feed38f9815d75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ornotto-0.1.1-py3-none-manylinux_2_28_x86_64.whl
| Download URL | ornotto-0.1.1-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 6.0 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
777eb2624cc294cd9bd328ea3f00ad32a9e62503c695e25dda3d292075f45309
|
|
BLAKE2b-256 checksum How to use checksums |
15598842d1ba66a7ae695e8120262ccb998816a329bf7e094cf468ce8664c5f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ornotto-0.1.1-py3-none-manylinux_2_28_aarch64.whl
| Download URL | ornotto-0.1.1-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 5.7 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
433aa6662b0edfbbfcf1e2db84dca44eebd4eac6f7e5e5eac3391384201bce90
|
|
BLAKE2b-256 checksum How to use checksums |
cf51bb90c55aa83f1bcbb515e2b21023af5e1ae0e17d4531a45fea010c66906f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ornotto-0.1.1-py3-none-macosx_14_0_arm64.whl
| Download URL | ornotto-0.1.1-py3-none-macosx_14_0_arm64.whl |
|---|---|
| Size | 5.5 MB |
| Tags | Python 3 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
34ee5055619ffca8a484aff2669687a9dc1a70424eb1c44e15ad4e977eb3602d
|
|
BLAKE2b-256 checksum How to use checksums |
b2512ba92b80564e138d0045f97cc22a250054386df85d20334a406e8f4766c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|