Skip to main content

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)

Source distribution for ornotto 0.1.1
File Size Uploaded
ornotto-0.1.1.tar.gz 34.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for ornotto 0.1.1
File
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}

Release history Release notifications | RSS feed

This release

0.1.1 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page