Skip to main content

decidr

Typed decisions from local LLMs, in one forward pass. No generated sentence, no JSON to parse, no decoding loop.

from decidr import Client

client = Client(model="qwen3.5:4b")

d = client.decide({
    "id": "route-1",
    "state": "Customer cannot access an account after a password reset. The reset email never arrived.",
    "question": "Which queue should handle this request?",
    "options": [
        {"id": "access",  "description": "Account access and authentication support."},
        {"id": "billing", "description": "Billing and payment support."},
        {"id": "sales",   "description": "Sales and product evaluation."},
    ],
})

d.choice          # 'access'
d.confidence      # 0.9779
d.probabilities   # {'access': 0.9779, 'billing': 0.0133, 'sales': 0.0088}

Works with any model already pulled in Ollama. No fine-tuning, no extra runtime.

Install

pip install decidr

Python 3.10+, a running Ollama. Zero dependencies.

How it works

A model computes a probability over its whole vocabulary before it samples anything. Reading that distribution directly, instead of generating text and parsing it back into a decision, skips the entire decoding step. decidr sends the options, runs one forward pass, and reads the probability of each option straight out of the model's logits.

In prefix mode (the default), option ids are scored by their own text rather than a stand-in letter. Since there's no way to ask Ollama how a string tokenizes, boundaries are discovered by reading what the model actually returns and matching it against each option's id, one piece at a time; options that share a prefix get resolved together over more than one request. Full mechanism, including why every option is walked to the end of its own id rather than stopped as soon as it's distinguishable from the others: docs/PREFIX_MATCHING.md.

Two modes

Mode Default Requests per decision Option count
prefix yes 1, or more only when options collide unlimited
letters no always 1 up to 16
Client(model="qwen3.5:4b")                    # prefix, the default
Client(model="qwen3.5:4b", id_mode="letters")  # fixed cost, capped option count

letters is the older, simpler path: each option is shown to the model as A, B, C, and the meaning stays in the description. It's a fixed one request per decision, always, but the option count is capped at 16 because there are only so many letters.

Both modes read scores from top_logprobs, capped at 20 by Ollama's API. If an option's real next token doesn't land in that top-20 window, it's reported in decision.unscored rather than assigned a guessed value. A separate change proposed upstream to Ollama removes that cap by letting a request name the exact tokens it wants scored, regardless of rank: ollama/ollama#18579, ollama/ollama#18580. Not merged yet. Both modes work today without it and pick it up automatically if it lands, since decidr probes for the capability once per process.

Calibration

Probabilities are conditional on the option set supplied, and are not calibrated by default. choice is usually right. confidence on its own is not a true probability until it's checked against real outcomes.

decidr.calibrate fits one temperature to labeled data and rescales:

from decidr.calibrate import fit_temperature, evaluate_out_of_fold

labeled = [(client.decide(row), row["correct_id"]) for row in labeled_rows]

fit_temperature(labeled)
# CalibrationResult(temperature=1.8, n=200, ece_before=0.15, ece_after=0.06, accuracy=0.83)

evaluate_out_of_fold(labeled, folds=5)
# fits T on 4 folds, scores it on the 5th, repeated for every fold.
# use this number, not fit_temperature's own -- a temperature can look
# better than it is by overfitting to the exact sample it was fit on.

Rescaling by a constant can't change which option wins, so choice stays fixed and only confidence moves. Without labeled data, confidence still ranks options correctly relative to each other; treat the absolute number as approximate.

API

Client(model, host="http://127.0.0.1:11434", temperature=1.0, id_mode="prefix", force_mode=None)

id_mode is "prefix" or "letters", see above. temperature scales the softmax over option logprobs, not sampling. force_mode skips the capability probe and pins "exact" or "ranked".

client.decide(row) -> Decision

row needs id, state, question, and 2+ options (max 16 in letters mode), each with an id and a description. state may be a string, dict, or list. A malformed row raises DecisionError naming the problem.

Decision

Field
choice option id with the highest probability
confidence probability of choice
probabilities option id → probability, sums to 1 over scored options
logprobs raw log probabilities before normalizing
mode "prefix", "exact", or "ranked"
unscored options that could not be resolved
is_reliable() False when anything went unscored
raw_answer the model's own first reply

client.decide_all(rows) runs a list sequentially.

What's new

v0.3 — real option ids by default (prefix mode), replacing the single-letter scheme as the default path. Removes the 16-option cap for the common case. Adds decidr.calibrate for fitting confidence against labeled data.

Limitations

  • Sequential, no batching.
  • prefix mode's request count is variable: 1 for options that don't collide, more when they do, bounded by a depth cap.
  • letters mode is capped at 16 options and always costs exactly 1 request.
  • Both modes lose an option to unscored if its real next token falls outside top_logprobs's top-20 window, which gets more likely as option count or ambiguity grows.
  • Confidence uncalibrated by default; fixable with decidr.calibrate and labeled data.
  • Tested against qwen3.5:4b. Small models (under 1B parameters) often won't treat a bare option id as a plausible next token at all.

What this is not

  • Not a new model. It's a way of reading models already running.
  • Not a fine-tune. Nothing is trained; taxonomies change per request.
  • Not novel as a technique. Reading option logits in a single pass is established; TypeSafe's Jev and SemIf (an open-source alternative with far more benchmarking and calibration work than this project has) both do it. decidr's claim is narrow: the smallest version that runs against an Ollama already installed, with zero dependencies.

References

License

MIT

Metadata

Release files for decidr 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for decidr 0.3.0
File Size Uploaded
decidr-0.3.0.tar.gz 24.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for decidr 0.3.0
File Interpreter ABI Platform
decidr-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 40.3 kB

Release files / decidr-0.3.0.tar.gz

Download URL decidr-0.3.0.tar.gz
Size 24.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d370881314a2d497932c323b78688f0803b4df592bb3b458a47e8bef4252f84a
BLAKE2b-256 checksum
How to use checksums
c9873ac5321696d4d459bd85de50a0dffa3856bb66a4ca69c8de06473ee11ec4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / decidr-0.3.0-py3-none-any.whl

Download URL decidr-0.3.0-py3-none-any.whl
Size 16.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
96b9c7d444f3c802d310b29ff65616a8dcb39b48bbeabb03e60ff3218869e6ac
BLAKE2b-256 checksum
How to use checksums
54faf38fad2f1a3006f4120c93bb200c7d09c5358ccb3c1b9cfb12196ec8a1a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

0.9.0

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 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