decision-toolkit
Local-first toolkit around open-weights decision models: typed choice /
yes-no (noul) / score questions over a state (text or JSON), answered with a
calibrated probability for every option — no generated text, no parsing.
One ONNX engine (onnxruntime), in-process, on CPU. One adapter per model
contract (how a model is fed and read). Three thin surfaces over the same API:
library, CLI and REST.
Co-developed by GIA-UH and Syalia S.R.L..
Documentation: https://uhgia.org/decision-toolkit/. Start with
Examples for the library, REST
and CLI. dtk --help and every subcommand help also include runnable examples.
Installation
pip install decision-toolkit # library + CLI (0.1.3 on PyPI)
pip install "decision-toolkit[onnx]" # + onnxruntime, tokenizers (the model runtime)
pip install "decision-toolkit[server]" # + REST server (FastAPI)
PyPI: https://pypi.org/project/decision-toolkit/0.1.3/. See docs/RELEASE.md for the Trusted Publishing flow and clean-install checks.
Three ways to use it
1. As a library (primary interface)
from decision_toolkit import DecisionModel, choice, noul, score
# download once: dtk models download gliner2.5-multi
model = DecisionModel("gliner2.5-multi")
answer = model.decide(
state="Meeting with Acme: we agreed a 10% discount and will sign next week.",
questions={
"cat": choice("What kind of record is this?", {
"meeting": "a meeting or call with topics, decisions and actions",
"tasks": "things to do, with dates or owners",
"contact": "a person's details: name, role, company, phone, email",
"idea": "an idea, reflection, observation or question",
"other": "a greeting, small talk or anything else",
}),
"urgent": noul("Does it need a reply today?"),
},
)
answer["cat"].choice # 'meeting'
answer["cat"].confidence # 0.77
answer["urgent"].noul # probability of "true"
verdict = model.classify(
"Remind me: dentist next Tuesday at 9 am.",
{"reminder": "an event with a date or time",
"task": "a list of things to do",
"idea": "an idea or reflection"},
threshold=0.6,
)
verdict.label, verdict.confidence, verdict.below_threshold
The core is the superset of the surfaces. It also exposes lifecycle, usage, batch/async, capabilities and structured events:
from decision_toolkit import DecisionModel, request_context, noul
with DecisionModel("gliner2.5-multi", strict=True) as model:
model.warmup()
with request_context("req-42"):
decision = model.decide("Customer wants a refund today.",
{"urgent": noul("Does it need a reply today?")})
batch = model.decide_many(["first state", "second state"],
{"urgent": noul("Today?")})
decision["urgent"].confidence # normalized probability
decision.usage.to_dict() # input/output tokens + estimated flag
decision.latency_ms # per-decision latency
decision.request_id # 'req-42'
decision.truncated, decision.warnings
strict=True turns silent truncation into TruncationError. Capabilities are
validated before inference (box/assets are rejected by text-only models).
Custom adapters can be registered with register_adapter; local ONNX models
can be opened with DecisionModel.from_onnx(...). See
docs/CORE.md for the full phase 1 contract, error taxonomy,
logging events and the expressiveness inventory.
2. As a CLI
dtk models list --adapter --quant # catalog, variants and cache state
dtk models variants gliner2.5-multi --json # real variants (quant, size, role)
dtk models download gliner2.5-multi --dry-run # show the download first
dtk models use gliner2.5-multi --variant fp16
dtk models current --json # includes the current variant
dtk decide --model gliner2.5-multi \
--state "Meeting with Acme: 10% discount, sign next week." \
--questions-file questions.yaml --json
dtk decide --model gliner2.5-multi \
--input-jsonl states.jsonl --questions-file questions.yaml \
--output-jsonl decisions.jsonl --batch-size 32
dtk classify --model gliner2.5-multi \
--text "Dentist next Tuesday at 9" --labels "reminder,task,idea" \
--threshold 0.6 --json
dtk config list --json
dtk models verify --all --json
dtk has consistent exit codes (0 ok, 1 runtime, 2 usage, 3
verification), --json everywhere, quiet/verbose modes and no tracebacks for
expected failures. See docs/CLI.md for questions files, JSONL
batch, custom --adapter/--files, config and completion.
3. As a REST service (optional extra)
pip install "decision-toolkit[server]"
dtk serve --model gliner2.5-multi --warmup \
--api-key "$DECISION_TOOLKIT_API_KEY" \
--log-format json
Endpoints: POST /v1/systemone (System One 1.14), POST /v1/decisions
(OpenAI Decisions beta), GET /v1/models (OpenAI shape),
GET /v1/decision-models (rich catalog), GET /v1/info, GET /health,
GET /ready. Requests may select model and variant; failures use the OpenAI
{"error": {...}} envelope and every response carries x-request-id.
TYPESAFE_BASE_URL=http://localhost:8000 makes existing System One clients work.
See docs/REST.md for the pinned contracts, limits, auth and
the standard-evolution process.
The catalog is curated
The catalog is ours, not a mirror of HuggingFace. A model enters only when it is understood and verified:
- clean commercial license;
- an adapter whose contract we can implement from a real export;
- a real ONNX export on the Hub;
- measured parity against the original on a fixed sample;
- CPU-viable latency.
Every variant points to files that exist. No placeholders, no speculative adapters (there is no generic "any ONNX" adapter).
| id | adapter | status | default variant |
|---|---|---|---|
gliner2.5-multi |
gliner2 |
m0 — verified end-to-end (9/15, 353 ms) | fp16 (577 MB) |
gliner2.5-decide |
gliner2-decide |
m0 — fused export verified (refund 0.944, 912 ms; English-only) | q4f16 (524 MB) |
laya |
laya |
m0 — verified against torch (Δp ≤ 0.005) | q8 (528 MB) |
julia-1 |
julia |
m0 — verified: 100/100 argmax parity, 329 ms | fp32 (618 MB) |
opendecider-nano |
opendecider |
m0 — official build; prompt ported from pinned code (530 ms) | q8f16 (469 MB) |
Only m0 models are in the catalog. Researched-but-unavailable models
(planned, conversion pending, broken exports) are listed in
docs/ROADMAP.md and are not downloadable through the toolkit.
Per-model cards with the contract, measured evidence and known limitations: docs/models.
Variants and quantization
A quantization is a variant of a model, with its real repo, files and role:
gliner2.5-multi:
fp16: encoder_fp16.onnx + classifier_fp32.onnx # 577 MB default
fp32: encoder_fp32.onnx + classifier_fp32.onnx # 1132 MB quality
dtk models download <id>gets the default variant;--variant fp32gets another.- A variant becomes default only after passing the parity gate (top-1 ≥ 99%, mean |Δp| ≤ 0.02, accuracy delta ≤ 1 pp against the fp32 build on a fixed sample) and measuring latency on CPU.
- Unverified variants are never default. No silent fallback.
Operations
The server has bounded inference concurrency (--max-concurrent), an optional
per-key rate limiter (--rate-limit, 429 + Retry-After), Prometheus metrics
at /metrics, and optional OpenTelemetry spans (--tracing). Saturation
returns 503 server_busy instead of building an unbounded queue. State and
question text never become log/metric labels.
dtk serve --model gliner2.5-multi --warmup --max-concurrent 4 \
--rate-limit 120 --rate-window 60
PYTHONPATH=src python bench/load.py --base-url http://127.0.0.1:8000 \
--concurrency 10 --requests 30
See docs/OPERATIONS.md for metric names, alerts, the runbook and the multi-worker caveat.
Reproducibility
Every catalog variant is pinned to a real HuggingFace commit SHA and carries
a SHA256 per file, including external .onnx.data files. Downloads reject
mismatches and verification treats the catalog hashes as the source of truth:
dtk models verify --all --json # exit 3 if any file/hash/session fails
Latency/parity/calibration evidence is reproducible with the harness in
bench/ and documented in docs/BENCH.md. CI runs
lint, unit/contract tests and a CLI smoke from source on Python 3.12/3.13.
There is no wheel or PyPI release yet; packaging is a later milestone. See CHANGELOG.md, CONTRIBUTING.md, SECURITY.md and THIRD_PARTY_NOTICES.md.
Custom ONNX (at your own risk)
You can run any ONNX file with an adapter you trust — outside the curated catalog:
model = DecisionModel.from_onnx(
"./my-model",
adapter="gliner2", # the layout you declare
files={"encoder": "encoder.onnx", "classifier": "classifier.onnx",
"tokenizer": "tokenizer.json"},
)
# no parity, no license check, no support
Third-party layouts can be registered in-process:
from decision_toolkit import register_adapter
register_adapter("my-layout", MyAdapter)
Custom models do not appear in dtk models list; the toolkit never guesses a
layout. If a model deserves support, it goes through curation.
Development
python -m venv .venv && . .venv/bin/activate
pip install pytest ruff PyYAML huggingface_hub fastapi httpx pydantic numpy
PYTHONPATH=src pytest -q # fast suite (no model downloads)
PYTHONPATH=src pytest -m integration -o addopts="" # real model, opt-in
No wheel or editable install is required while the project is pre-distribution. CI runs the same source-tree commands on Python 3.12/3.13. See CONTRIBUTING.md.
License
MIT — see LICENSE.
Metadata
Release files for decision-toolkit 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| decision_toolkit-0.1.3.tar.gz | 136.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| decision_toolkit-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 231.3 kB
Release files / decision_toolkit-0.1.3.tar.gz
| Download URL | decision_toolkit-0.1.3.tar.gz |
|---|---|
| Size | 136.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c60138af04e0176b985811cb2623361b52440bd1e2d56b5aa7b23d3aa6255c16
|
|
BLAKE2b-256 checksum How to use checksums |
fb6b06b3f51adc934aefcb6d9e0f8e23facff94da9be051919c9fd333bcfc195
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logRelease files / decision_toolkit-0.1.3-py3-none-any.whl
| Download URL | decision_toolkit-0.1.3-py3-none-any.whl |
|---|---|
| Size | 95.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
79b0321f4b96968eb92b012a30462754a11f36c44953bd57059a19bdd94c01a3
|
|
BLAKE2b-256 checksum How to use checksums |
deed0ec9c7e8e372e23228ff04e1ddb5ffb1574ea15d7064df26b178df1ce6b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logSecurity for decision-toolkit 0.1.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for decision_toolkit-0.1.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.