Image-Text-to-Text
Transformers
Safetensors
GGUF
qwen3_5
system-one
decision-making
classification
calibration
structured-output
on-device
multimodal
Instructions to use bynn-intelligence-com/decisionnode-edge with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use bynn-intelligence-com/decisionnode-edge with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="bynn-intelligence-com/decisionnode-edge")# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("bynn-intelligence-com/decisionnode-edge") model = AutoModelForMultimodalLM.from_pretrained("bynn-intelligence-com/decisionnode-edge", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use bynn-intelligence-com/decisionnode-edge with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: llama cli -hf bynn-intelligence-com/decisionnode-edge:Q8_0
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: llama cli -hf bynn-intelligence-com/decisionnode-edge:Q8_0
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: ./llama-cli -hf bynn-intelligence-com/decisionnode-edge:Q8_0
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: ./build/bin/llama-cli -hf bynn-intelligence-com/decisionnode-edge:Q8_0
Use Docker
docker model run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- Llama new
- LM Studio
- Jan
- vLLM
How to use bynn-intelligence-com/decisionnode-edge with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "bynn-intelligence-com/decisionnode-edge" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "bynn-intelligence-com/decisionnode-edge", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- SGLang
How to use bynn-intelligence-com/decisionnode-edge with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "bynn-intelligence-com/decisionnode-edge" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "bynn-intelligence-com/decisionnode-edge", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "bynn-intelligence-com/decisionnode-edge" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "bynn-intelligence-com/decisionnode-edge", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Ollama
How to use bynn-intelligence-com/decisionnode-edge with Ollama:
ollama run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- Unsloth Desktop
- Docker Model Runner
How to use bynn-intelligence-com/decisionnode-edge with Docker Model Runner:
docker model run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- Lemonade
How to use bynn-intelligence-com/decisionnode-edge with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull bynn-intelligence-com/decisionnode-edge:Q8_0
Run and chat with the model
lemonade run user.decisionnode-edge-Q8_0
List all available models
lemonade list
- Atomic Chat
File size: 23,904 Bytes
071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 3dd48fe 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | # DecisionNode Edge runtime
The official runtime for the DecisionNode Edge models from Bynn Intelligence. Load a model, send a request in the DecisionNode API format, and receive typed, calibrated decisions. It runs on NVIDIA GPUs, Apple silicon and CPUs, entirely offline.
Models: [DecisionNode Edge](https://huggingface.co/bynn-intelligence-com/decisionnode-edge) and [DecisionNode Edge Mini](https://huggingface.co/bynn-intelligence-com/decisionnode-edge-mini) on Hugging Face.
## Install
```bash
pip install decisionnode-edge # torch, transformers, safetensors
pip install "decisionnode-edge[images]" # adds Pillow and torchvision, for pictures
```
The model folders carry a copy of this package in `runtime/` for machines without network access: `pip install ./runtime` (or `"./runtime[images]"`) from the model folder installs the same thing.
Optional, for speed on NVIDIA GPUs with the PyTorch backend: `flash-linear-attention` and `causal-conv1d`. transformers uses them when they are installed. Without them the model gives the same answers about twice as slowly on long inputs.
### The fastest install for your device
The runtime reads the model with PyTorch unless a faster backend is installed. Install the one for your device:
| Device | Install | Backend |
|---|---|---|
| NVIDIA GPU (Linux) | `pip install "decisionnode-edge[vllm]"` | `vllm` |
| x86 CPU | `pip install "decisionnode-edge[openvino]"`, then build the OpenVINO files once ([below](#building-what-a-backend-loads)) | `openvino` |
| Apple silicon | `pip install "decisionnode-edge[mlx]"` | `mlx` |
| Any device llama.cpp supports, including NVIDIA Jetson | llama.cpp's `llama-server`; the model folder ships the 8-bit GGUF file ([below](#building-what-a-backend-loads)) | `llamacpp` |
Every backend agrees with the PyTorch reference on at least 98% of top answers in our tests, and the remaining differences fall on near ties. A backend only computes the model's last hidden state; the runtime then applies the model's own answer table (`readout.safetensors`) and calibration (`calibration.json`) in float32, as it does on PyTorch.
Measured with one `truth` question per request, one request at a time, median time of the whole `decide` call:
| Device (as measured) | Backend | Edge, about 500 / 2,000 tokens | Edge Mini | Answers kept (Edge, Edge Mini) | PyTorch on the same device (Edge; Edge Mini) |
|---|---|---|---|---|---|
| NVIDIA RTX PRO 6000 Blackwell | `vllm` | 9.5 / 23 ms | 7.3 / 15 ms | 99.7%, 99.2% | 14 / 28 ms; 14 / 16 ms (with the optional kernels) |
| AMD Ryzen 7 9800X3D (8 cores) | `openvino` | 0.46 / 1.7 s | 0.24 / 0.90 s | 98.6%, 98.4% | bfloat16: 0.49 / 2.6 s; 0.28 / 1.4 s. float32, the default: 1.6 / 7.8 s; 0.66 / 4.0 s |
| Apple M5 Max | `mlx` | 35 / 127 ms | 17 / 65 ms | 99.5%, 99.3% | `device="mps"`: 0.30 / 1.3 s; 0.28 / 1.2 s |
| Apple M5 Max | `llamacpp` (Metal) | 55 / 212 ms | 30 / 112 ms | 99.4%, 99.0% | |
| NVIDIA RTX PRO 6000 Blackwell | `llamacpp` (CUDA) | 14 / 44 ms | 10.5 / 30 ms | 99.2%, 98.4% | |
| AMD Ryzen 7 9800X3D (8 cores) | `llamacpp` (CPU) | 1.1 / 5.1 s | 0.58 / 2.7 s | 99.8%, 98.1% (412 questions up to 512 tokens) | |
At about 8,000 tokens: `vllm` 87 ms (Edge) and 50 ms (Edge Mini), `mlx` 625 and 297 ms, `llamacpp` 218 and 160 ms on CUDA and 0.92 and 0.55 s on Metal. OpenVINO hands prompts of that length to PyTorch.
"Answers kept" is how often the backend's top answer equals the PyTorch reference (bfloat16 on an NVIDIA GPU) on a fixed set of 2,056 held-out questions; for OpenVINO, on the 1,936 of them up to 2,048 tokens, the longest it is built for.
The GPU is an NVIDIA RTX PRO 6000 Blackwell workstation card, the CPU an AMD Ryzen 7 9800X3D (8 cores, 16 threads), the Mac an Apple M5 Max. Versions: vLLM 0.32.0, OpenVINO 2026.4.1, MLX 0.32.4 with mlx-lm 0.32.2, llama.cpp at commit f174553, PyTorch 2.13 and 2.14, transformers 5.18 and 5.19. All backends ran in bfloat16 except llama.cpp, which ran the 8-bit (Q8_0) file.
## Use
```python
from decisionnode_edge import EdgeModel
model = EdgeModel.load("path/to/decisionnode-edge") # CUDA if present, else the CPU
response = model.decide({
"state": {"ticket": "I was charged twice and nobody has replied for 3 days."},
"questions": {
"route": {
"type": "choice",
"instructions": "Where should `ticket` go?",
"criteria": {"billing": "money", "bug": "something broken", "account": "login and access"},
},
"urgency": {
"type": "score",
"instructions": "How urgent is `ticket`?",
"criteria": ["routine", "today", "urgent", "critical"],
},
"escalate": {"type": "truth", "instructions": "Should a human take over now?"},
"days": {"type": "number", "instructions": "How many days has the customer waited?", "min": 0, "max": 30},
},
})
response["answers"]["route"] # {"type": "choice", "choice": ..., "confidence": ..., "probabilities": {...}}
response["answers"]["escalate"] # {"type": "truth", "truth": ...}
```
`model.predict(request)` gives the unrounded calibrated probabilities per question instead: one per option or level in request order, `[yes, no]` for a truth question, one per grid value for a number question. Point and box questions are answered by `decide` only.
A response names the model that answered: `decisionnode-edge-0.9.1` for DecisionNode Edge, `decisionnode-edge-mini-0.9.1` for DecisionNode Edge Mini (the `name` in the model directory's `decisionnode_edge.json`).
From the command line:
```bash
python -m decisionnode_edge path/to/decisionnode-edge request.json
cat request.json | python -m decisionnode_edge path/to/decisionnode-edge -
```
### Serving over HTTP
```bash
python -m decisionnode_edge serve path/to/decisionnode-edge --host 127.0.0.1 --port 8000
python -m decisionnode_edge serve path/to/decisionnode-edge --api-key key.txt # callers send Authorization: Bearer <key>
```
A small server on Python's standard library, with nothing more to install. It loads the model with the backend `EdgeModel.load` picks (or `--backend`, `--artifacts`, `--device`, `--dtype`), answers one request at a time, and listens on 127.0.0.1 unless `--host` says otherwise.
| Route | What |
|---|---|
| `POST /v1/systemone` | the `/v1/systemone` format used by public decision-model leaderboards: `{"model", "state", "questions", "assets"}` with `noul`, `choice`, `score` and `box` questions. A `noul` is answered `{"type": "noul", "noul": p}`, a choice and a score as on `/v1/decide` with probabilities that sum to exactly 1, a box as below. `model` is required, and any name is accepted: a local server has no model catalogue, and the answer names the model that answered. Number and point questions go to `/v1/decide` |
| `POST /v1/decide` | the request and answers of this runtime ([below](#the-request)), `number`, `truth`, `point` and `box` included |
| `GET /v1/models` | `{"models": [{"name", "description", "release_date"}]}` |
| `GET /healthz` | `{"status": "ok", "model", "backend"}`, no key needed |
Every answer carries `usage`: `input_tokens` and `output_tokens` (always 0). Pictures go as base64: the runtime's `images` with `data` (a local `path` is refused over HTTP, so a caller can never make the server read a file), or, on `/v1/systemone`, the format's own:
- an `assets` map, `{"<ref>": {"type": "image", "format": "png", "data_base64": "..."}}` with format `png`, `jpg` or `jpeg` (the bytes decide when they say otherwise). Wherever a reference appears in the state, an instruction or a criterion, the text reads `Picture <n>` and the asset becomes picture n (`image` when it is the only one, else `image_<n>`), numbered in the order the references first appear. An asset no text refers to is refused. A score's `legend` shows its levels as sent, references included.
- `image_url` content parts (`{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}`) in the same places, and `images` as a list of data URLs.
This is the 0.9.2 behaviour, the same as the hosted `/v1/systemone`. In 0.9.1 each asset became a picture whose id was its reference, and the text stayed as sent. `/v1/decide` reads pictures as before.
A `box` question on `/v1/systemone` is `{"type": "box", "instructions": "..."}`. Its answer is `{"type": "box", "box": {"top", "bottom", "left", "right"}, "confidence"}`, the edges as fractions of the picture's height and width. When the request holds several pictures, the box looks in the one a path in backticks names in the state (`` `photo` ``, `` `cams.door` ``), else in the first picture of the state; with the runtime's own `images`, name it with `image`, as on `/v1/decide`. A box with `max` keeps the list answer of `/v1/decide`.
`confidence` is the probability that the box is right (an intersection over union of 0.5 or more with the true box) when the model folder holds a box calibration, `box_calibration.json`. Without one it is `present`, the probability that the thing is in the picture. The server reads the file once at start and refuses to start on a file it cannot check. `--box-calibration FILE` uses another file and `--box-calibration none` uses none; `--box-calibration-sha256` (or `box_calibration_sha256` in `decisionnode_edge.json`) pins the file's sha256. `/v1/decide` never reads it.
Errors come as `{"detail": {"error_type": ..., "message": ...}}` (a 422 as a list of field errors). A request longer than the model's limit is a 400 whose message says "maximum context length"; a choice with more than 255 options is a 400 that says "options per choice". On `/v1/systemone` these limits come in the hosted route's bodies, which benchmark harnesses read to record a request as beyond the model:
| Limit | `/v1/systemone` body |
|---|---|
| longer than the model's limit | `{"detail": {"error_type": "max_tokens_exceeded", "message": "The request does not fit the model's maximum context length (...)."}}` |
| more than 255 options | `{"detail": "Too many choices. Must have at most 255 choices (255 options per choice)."}` |
| more than 10 levels | `{"detail": "Too many score levels. Must have at most 10 levels (a score takes 2 to 10 levels; one level is answered without the model)."}` |
### Options
`EdgeModel.load(path, device=None, dtype=None, backend=None, artifacts=None, options=None)`:
- `device`: `"cuda"`, `"cuda:1"`, `"cpu"` or `"mps"`. Default: CUDA when present, else the CPU. It also sets the device class the backend is chosen for.
- `dtype`: `"bfloat16"` or `"float32"`, for PyTorch. Default: bfloat16 on a GPU (the reference precision) and float32 on a CPU. On CPUs with bfloat16 support (recent AMD and Intel parts) `dtype="bfloat16"` was about three times faster in our tests, with answers as close to the GPU's as float32 gives.
- `backend`: `"auto"` (the default), `"torch"`, `"vllm"`, `"openvino"`, `"mlx"` or `"llamacpp"`.
- `artifacts`: the directory the build tool wrote, for `openvino` and `llamacpp`.
- `options`: the backend's settings ([below](#backend-options)).
### Choosing a backend
By default `EdgeModel.load` takes the fastest backend that is installed and can run on the machine:
| Device class | Order |
|---|---|
| NVIDIA GPU | `vllm`, `llamacpp`, `torch` |
| Apple silicon | `mlx`, `llamacpp`, `torch` |
| Any other CPU | `openvino`, `llamacpp`, `torch` |
`openvino` counts only when `artifacts` holds an OpenVINO build, and `llamacpp` only when `artifacts` holds a GGUF build and `llama-server` is found (or `options` names a running server). Asked for by name, `openvino` and `llamacpp` also find a build in the model folder itself, such as the GGUF file that ships there. `model.backend` says which one was taken.
To choose yourself:
```python
model = EdgeModel.load("path/to/decisionnode-edge", backend="torch")
model = EdgeModel.load("path/to/decisionnode-edge", backend="openvino", artifacts="path/to/edge-openvino")
model = EdgeModel.load("path/to/decisionnode-edge", backend="llamacpp", options={"url": "http://127.0.0.1:8080"})
```
The same from the environment: `DECISIONNODE_EDGE_BACKEND`, `DECISIONNODE_EDGE_ARTIFACTS`, and `DECISIONNODE_EDGE_OPTIONS` (a JSON object). Arguments win over the environment. On the command line: `python -m decisionnode_edge path/to/decisionnode-edge request.json --backend openvino --artifacts path/to/edge-openvino`.
The faster backends read text. PyTorch reads what they do not take: pictures, and prompts longer than the backend's limit (an OpenVINO build's longest length, 2,048 tokens by default; a llama.cpp context, 16,384 tokens by default). It is loaded the first time it is needed, on `device` in `dtype`, and stays loaded. If most of your requests carry pictures, use `backend="torch"`.
### Building what a backend loads
The build tools read the model directory and write a new directory. They never change the model directory.
**OpenVINO.** One file per static input length, 512 and 2,048 tokens by default:
```bash
pip install "decisionnode-edge[openvino]"
python -m decisionnode_edge.build openvino path/to/decisionnode-edge --out path/to/edge-openvino
python -m decisionnode_edge.build openvino path/to/decisionnode-edge --out path/to/edge-openvino --lengths 512,1024,2048
```
The export runs on the GPU when there is one (`--device cuda`, then the default) and on the CPU otherwise. On our machine it took 14 minutes for 512 tokens and 19 minutes for 2,048, per length. The files keep the weights in float32 (exact copies of the bfloat16 weights) and OpenVINO runs them in bfloat16, so the build takes 6.2 GB for Edge Mini and 15.2 GB for Edge. Before it ends, the tool runs each file once against PyTorch.
A prompt is padded to the next length it fits, so a 600-token prompt costs about what a 2,048-token one does. Add a length (for example `--lengths 512,1024,2048`) if many of your prompts fall between two lengths. Lengths above a few thousand tokens take hours to export; longer prompts go to PyTorch.
**GGUF for llama.cpp.** The model folder ships the 8-bit file (`decisionnode-edge-0.9.1-q8_0.gguf`, `decisionnode-edge-mini-0.9.1-q8_0.gguf`) with its build record, so `EdgeModel.load("path/to/decisionnode-edge", backend="llamacpp")` uses it with no build step. To make it yourself (8-bit keeps the answers; 4-bit does not, so the tool does not offer it):
```bash
git clone https://github.com/ggml-org/llama.cpp
cmake -S llama.cpp -B llama.cpp/build -DCMAKE_BUILD_TYPE=Release # add -DGGML_CUDA=ON on an NVIDIA GPU or a Jetson
cmake --build llama.cpp/build --target llama-server -j
pip install -r llama.cpp/requirements/requirements-convert_hf_to_gguf.txt
python -m decisionnode_edge.build gguf path/to/decisionnode-edge --out path/to/edge-gguf --llama-cpp llama.cpp
```
The conversion takes seconds and writes one file: 2.0 GB for Edge, 0.8 GB for Edge Mini. The same file serves a CPU, an NVIDIA GPU, a Jetson and a Mac; llama.cpp itself is built for each.
The runtime then starts `llama-server` on 127.0.0.1 itself (from your `PATH`, or `options={"server": "path/to/llama-server"}`) and stops it with the model. To run the server yourself, start it with the embedding endpoint and the last-position pooling, with a batch as large as the context:
```bash
llama-server -m edge-gguf/model-q8_0.gguf --embeddings --pooling last -c 16384 -b 16384 -ub 16384 -ngl 999 --port 8080
```
and pass `options={"url": "http://127.0.0.1:8080"}`. The runtime sends token ids, so llama.cpp's own tokenizer is never used.
### Backend options
Options can be flat (`{"threads": 16}`) or under a backend's name (`{"openvino": {"threads": 16}}`), so one set of options can serve whichever backend is taken.
| Backend | Option | Default |
|---|---|---|
| `vllm` | any vLLM engine argument, such as `gpu_memory_utilization` or `enforce_eager` | a 2 GiB KV cache; the share of the GPU that covers the weights, that cache and 4 GiB for activations |
| `openvino` | `threads` | OpenVINO's choice |
| `openvino` | `precision`: `"bf16"` or `"f32"` | `"bf16"` (OpenVINO falls back to f32 on CPUs without bfloat16) |
| `openvino` | `config`: other OpenVINO CPU properties | |
| `llamacpp` | `url`: a running `llama-server` | start one |
| `llamacpp` | `server`, `gguf`: the program and the model file | `llama-server` on `PATH`, the build's file |
| `llamacpp` | `gpu_layers`, `threads` | 999 (all layers on the GPU when there is one), llama.cpp's choice |
| `llamacpp` | `context`: the longest prompt llama.cpp takes | 16,384 |
| `mlx` | `cache_limit_gb`: how much freed GPU memory MLX keeps for reuse | 2 |
vLLM runs its engine inside your process and uses the first visible GPU (choose it with `CUDA_VISIBLE_DEVICES`). Its first load takes about half a minute while it compiles. MLX uses the Apple GPU.
### Pictures
Send pictures beside the state, up to 16, each with an `id`:
```python
{"images": [{"id": "receipt", "data": "<base64>", "media_type": "image/jpeg"}]} # as with the hosted API
{"images": [{"id": "receipt", "path": "/data/receipt.jpg"}]} # a local file, Edge only
```
A picture is turned upright from its EXIF orientation, converted to RGB and scaled so its longest side is at most 2,048 pixels. Every question sees every picture. The prompt names a picture `Picture <n> (<id>)`, so instructions can refer to "Picture 2" or to "the receipt".
### Points and boxes
A `point` question asks where something is in a picture, a `box` question asks for a rectangle around it. Add `max` to get every match as a list:
```python
response = model.decide({
"images": [{"id": "belt", "path": "/data/belt.jpg"}],
"questions": {
"pick": {"type": "point", "instructions": "The yellow package closest to the robot arm."},
"damage": {"type": "box", "instructions": "The damaged package.", "units": "normalized"},
"parcels": {"type": "box", "instructions": "Every parcel on the belt.", "max": 10},
},
})
response["answers"]["pick"]
# {"type": "point", "x": ..., "y": ..., "present": ..., "confidence": ..., "image": "belt", "width": 1920, "height": 1080}
response["answers"]["parcels"]
# {"type": "box", "count": ..., "present": ..., "complete": ..., "boxes": [{"x1": ..., "y1": ..., "x2": ..., "y2": ..., "confidence": ...}, ...],
# "image": "belt", "width": 1920, "height": 1080}
```
| Field | Notes |
|---|---|
| `instructions` | what to find, required: a phrase ("the yellow package"), a description with conditions, or a question ("Where is the crack?") |
| `image` | the id of the picture to look in; needed when the request has two or more pictures |
| `units` | `"pixels"` (the default): whole pixels of the picture as sent, after its EXIF orientation. `"normalized"`: fractions of the width and height with 4 decimals |
| `max` | 2 to 16: list up to that many places in place of one |
In an answer, `x` and `y` are measured from the top-left corner, `x` to the right and `y` down. A box is its top-left corner (`x1`, `y1`) and its bottom-right corner (`x2`, `y2`). `present` is the probability that the thing is in the picture at all; when it is low, the place is only the best guess. `confidence` says how sure the model is of the place: the product of the peak probabilities of its coordinate readings. It is not calibrated yet, so use it to compare answers, not as a probability. `image`, `width` and `height` name the picture and its size.
With `max`, `count` is the number of places listed, `complete` the probability that there are no more than `max`, and the places come from the left, ties from the top, each with its own `confidence`. Two boxes that overlap with an intersection over union of 0.7 or more are one thing: the less confident one is left out.
A point or box question takes a few short prompts on the same picture: whether the thing is there, then where it is across the width, then down the height at that spot, and for a box its width and height around that centre. A point takes 3 prompts, a box 5, a list of n boxes 2 + 4n. Each prompt reads the picture again, so a box costs about five times a `truth` question on the same picture. As with every question that carries pictures, PyTorch reads them whichever backend is chosen.
## The request
The request and the answers follow the DecisionNode API's `/v1/decide`:
| Field | Notes |
|---|---|
| `state` | a string (used as it is), an object or an array (compact JSON, key order kept); missing, null and `""` are the empty state |
| `questions` | id to question, at least one; the id is never shown to the model |
| `images` | optional, see above |
| `model` | ignored |
| Question type | Fields |
|---|---|
| `choice` | `instructions` (optional), `criteria`: {option: description}, 1 to 255 options; a description may be a string, an object, an array or null |
| `score` | `instructions` (optional), `criteria`: [level, ...] low to high, 1 to 10 levels |
| `truth` | `instructions` and/or `criteria`: {"true": ..., "false": ...} |
| `number` | `instructions`, `min`, `max`, `step` (defaults 0, 255, 1), up to 4,096 values; more than 256 values are answered in pages of 256 and composed |
| `point`, `box` | `instructions`, `image` (with two or more pictures), `units` (`pixels` or `normalized`), `max` (2 to 16, a list); see [Points and boxes](#points-and-boxes) |
Answers are rounded as the API rounds them: probabilities to 2 decimals (3 for number questions), with the largest entries moved by 0.01 until a list sums to 1.
A request the model cannot answer raises `decisionnode_edge.RequestError`, whose `status` is the HTTP status the hosted API gives the same fault (400 or 422). For point and box questions, `errors` lists every faulty field as the hosted API's 422 does. The runtime checks what the model needs; the hosted API's error bodies are not reproduced word for word. Requests longer than the model's limit (`limits.max_input_tokens` in `decisionnode_edge.json`, 16,384 tokens) are refused, never cut.
Not on Edge: videos, boosters, image URLs and uploads, sessions and batch jobs. These are features of the hosted API.
## How it works
Each question becomes one prompt: the state block (pictures first, then the state text), then the question block (the type, the instructions, then one line per option, each led by an answer code token), then the word `Answer` and an answer marker. The codes of a question are drawn from a hash of the question alone, so the same question always gets the same codes. The model reads the prompt in one forward pass, and the last hidden state is scored against the rows of `readout.safetensors` that belong to the question's codes (or to its yes and no tokens). `calibration.json` turns those scores into probabilities. The model never generates text.
A point or box question is a short series of such prompts. Its coordinate prompts list no options: they are answered by the 256 code tokens in order, code k meaning step k of 256 along a side of the picture, and the answer is the probability-weighted mean of the steps around the most probable one. A later prompt is given what the earlier ones read: the y is asked at the x already chosen, and a box's size around its centre.
The code is a few short modules: `template.py` (the prompt and the codes), `request.py` (reading a request), `answers.py` (calibration and the answer shapes), `places.py` (point and box questions), `backends.py` (what reads the backbone: PyTorch, vLLM, OpenVINO, MLX or llama.cpp), `build.py` (the OpenVINO and GGUF builds), `server.py` (the HTTP server) with `systemone.py` (the `/v1/systemone` format), and `model.py` tying them to the weights.
## License
The runtime code, like the model weights, is licensed under the GNU Affero General Public License, version 3 only (SPDX: `AGPL-3.0-only`). For use in closed products and services, Bynn Intelligence Inc. offers a commercial license: licensing@bynn.com.
|