Skip to main content

pydecide

One Python client for typed decisions, with a confidence gate between the cheap model and the good one.

CI PyPI Python versions Downloads License

A state and three typed questions enter tier one, a 22M parameter cross-encoder. A confidence gate sends the questions it was not sure about to tier two, Laya. The response carries both the answers and the route.

Routing, triage and guardrail calls do not need a chat model. A decision model takes a state and typed questions (pick one of these, score this, yes or no) and answers with probabilities, in milliseconds. pydecide speaks to all of them through one API: TypeSafe's hosted Jev, OpenRouter, OpenAI's Decisions endpoint, the open-weight laya family on PyTorch and MLX, any sentence-transformers CrossEncoder, and a JSON-prompted LLM as a last resort.

The reason to use it over any single backend's SDK is the chain. A Client takes an ordered list of backends and a confidence policy. A backend that errors, or that answers below its threshold, hands the question to the next one, so a local model carries the traffic and a hosted model sees only what the local one could not answer. Every response records the route it took.

One API, seven backends Local and hosted behind the same Choice, Score and Noul types.
Confidence gate Per call, per question, or per question type. Score answers are never gated, and Gating says why.
Local by default API keys never select a backend. Choose hosted fallback explicitly with decide init or Python configuration. Gating.
Threshold from data Record decisions, attach outcomes, decide fit prints what each threshold would have accepted and how often it was right.
Readable routes r.meta.route is ['crossencoder:low_confidence:team=0.44<0.60', 'laya:ok'], not a boolean.
Drop-in for typesafe-sdk Swap one import and keep your questions, calls and response objects. Limits.
A server and a CLI decide serve speaks TypeSafe's wire format; decide ask answers from the shell.
20.9 ms Median for one Choice question on an M4 with laya-mlx, in process. Measured, method in Latency.

Install

uv tool install pydecide   # the decide CLI on your PATH, from any directory
uv add pydecide            # inside a project

With pip: pip install pydecide / pipx install pydecide. That is the CLI, the hosted backends and one dependency, httpx. decide serve additionally needs the server extra.

Local model backends need an extra:

uv tool install "pydecide[all]"

With pip: pip install "pydecide[all]". Quote the brackets in zsh, since it otherwise tries to glob them. This pulls in PyTorch and MLX, about 2 GB.

Extra Adds
server FastAPI and uvicorn, for decide serve. The client alone needs neither.
laya laya backend (PyTorch)
mlx laya_mlx backend (Apple Silicon; needs Python 3.11+)
st crossencoder backend (sentence-transformers)
all laya + mlx + st

The mlx extra depends on laya-mlx, which requires Python 3.11 or newer; on 3.10 the extra installs nothing and the laya_mlx backend is unavailable.

Quickstart: zero configuration

Install a local backend and decide ask works right away, with no environment variables at all:

uv tool install "pydecide[mlx]"    # Apple Silicon
# or
uv tool install "pydecide[laya]"   # any platform (PyTorch)
decide ask "I had a rough day, everything broke" --noul "Is the writer doing well?" --choice good,bad
NAME    TYPE    ANSWER  PROBABILITIES
choice  choice  bad     good=0.01 bad=0.99
noul    noul    false   0.01
backend=laya_mlx route=laya_mlx:ok latency=56.5ms

This is the real output of that command, run with laya_mlx installed and none of DECIDE_LOCAL_MODEL, TYPESAFE_API_KEY, OPENROUTER_API_KEY, OPENAI_API_KEY, DECIDE_LLM_BASE_URL or DECIDE_BACKENDS set.

Run decide init to interactively save an explicit backend chain and confidence threshold in decide.json. Both the CLI and Python client read that file from the current directory. Setup never saves credentials or loads models.

The Python client works the same way. Client.from_env() reads the saved configuration, or discovers an installed local backend when there is no file. API keys alone never select a hosted backend. Pass an explicit env= mapping instead (e.g. in tests) to configure from something other than os.environ:

from decide import Client, Choice, Score, Noul

client = Client.from_env()

r = client.decide(
    state={"ticket": "I was charged twice, please refund."},
    questions={
        "team": Choice(
            "Which team handles this?",
            {
                "billing": "charges, refunds",
                "engineering": "bugs and outages",
                "sales": "pricing and upgrades",
            },
        ),
        "severity": Score("How severe is this?", ["minor", "degraded", "blocked"]),
        "refund": Noul("Does the customer ask for money back?"),
    },
)

r.choices["team"].choice  # "billing"
r.choices["team"].probabilities  # {"billing": 0.9763, "engineering": 0.0107, "sales": 0.013}
r.scores["severity"].score  # 1.6284  (expected level index, 0..len(levels)-1)
r.scores["severity"].probabilities  # [0.0802, 0.2112, 0.7086]
r.nouls["refund"].noul  # 0.8919
r.meta.backend  # "laya_mlx"
r.meta.latency_ms  # 50.0
r.meta.route  # ["laya_mlx:ok"]

This was run against the laya_mlx backend with no environment variables set at all, using its own default checkpoint; every value above is the real output of that run, not illustrative.

Python accepts the same configuration directly or by file:

from decide import Client, Config

client = Client(config="decide.json")
# Or configure in code:
client = Client(
    config=Config(
        backends=["laya_mlx", "typesafe"],
        min_confidence=0.85,
    )
)

That chain explicitly allows hosted fallback to TypeSafe; provide its credential through TYPESAFE_API_KEY. Without a config, only laya_mlx (preferred) or laya is discovered. DECIDE_BACKENDS remains an explicit override for from_env and the CLI. DECIDE_LOCAL_ONLY=1 refuses server backends even when explicitly selected. With no configured backend or local installation, the client raises ConfigError with setup instructions.

See Configuration for the file format and precedence rules.

AsyncClient has the same surface, awaited: await client.decide(...), await client.decide_batch(...), AsyncClient.from_env(...).

Documentation

Backends The seven backends, the environment variables that pick them, and running the typesafe backend against any other /v1/systemone server.
Gating and fallback How the chain escalates, choosing a threshold, and the entropy, margin, conformal and calibrated policies.
Server decide serve, which speaks TypeSafe's wire format so a non-Python service can call a local chain.
CLI decide ask and the rest of the command line.
Recording and fitting Record decisions in production, attach what happened, and fit a threshold off the result instead of guessing one.
Already using typesafe-sdk? The compatibility client, and what it does not support.

Latency

Measured on an Apple M4 (32 GB) with the laya_mlx backend (laya-mlx 0.4.0) on its default checkpoint, aac6fef/laya-mlx, in process, after three warm-up requests. One Client.decide call, timed by meta.latency_ms, 20 samples:

Request Median Range
One Choice question 20.9 ms 19.4 to 24.0 ms
Choice, Noul and Score in one call 38.1 ms 37.3 to 41.0 ms

Loading the model and answering the first request took 1,304 ms, once. A decide CLI invocation on the same machine takes 0.7 to 1.3 s of wall clock, most of it interpreter startup and loading the model again, while the decision inside it measures 56 to 144 ms. Measure in process if you are comparing against a hosted API.

Your numbers will differ with the machine, the checkpoint and the length of the state, so measure before you rely on any of this. --verbose prints the backend, the route and the latency of the call:

decide ask "I was charged twice" --choice billing,engineering --verbose
NAME    TYPE    ANSWER   PROBABILITIES
choice  choice  billing  billing=0.99 engineering=0.01
backend=laya_mlx route=laya_mlx:ok latency=45.4ms

That one is a cold call with no warm-up, which is why it is slower than the table above.

What the probabilities mean

Every probability in a ChoiceAnswer, ScoreAnswer or NoulAnswer is whatever the backend reported; decide does not calibrate, smooth or verify it. What that means differs by backend: typesafe, openrouter, openai_decisions and laya/laya_mlx are purpose-built decision models, but their outputs are still self-reported by the model and not audited by this library. The crossencoder backend turns a relevance reranker's raw logits into a softmax or sigmoid, so the result is a normalized score forced to distribute mass over the supplied candidates, not a calibrated probability; a Choice will still pick a winner even when every candidate is a bad fit, and a Noul of 0.9 does not mean the condition holds 90% of the time. The llm backend is the least trustworthy of all: it prompts a general chat model to estimate its own confidence in JSON, with no guarantee the model attends to every candidate or keeps its numbers well calibrated. Treat all of this accordingly, as a signal to gate and fall back on, not as ground truth.

Status

The public API may still change before a 1.0 release. The current version is the one in the PyPI badge above, and the changelog says what changed.

License

MIT, see LICENSE.

Metadata

Release files for pydecide 0.2.19

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

Source distribution (sdist)

Source distribution for pydecide 0.2.19
File Size Uploaded Provenance Actions
pydecide-0.2.19.tar.gz 313.7 kB With provenance Details for pydecide-0.2.19.tar.gz

Built distribution (wheel)

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

Total release size: 381.3 kB

Release files / pydecide-0.2.19.tar.gz

Download URL pydecide-0.2.19.tar.gz
Size 313.7 kB
Tags Source
SHA-256 checksum
How to use checksums
3a27c974944faf6d1624a97838912062f9ecff6b23cb7f4b5b9628b103b761c8
BLAKE2b-256 checksum
How to use checksums
1a4aa228f61c933347a424c80240c12b68e7a258ec90339451dbf3f7eaf133f4
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 21, 2026.

Transparency log

Release files / pydecide-0.2.19-py3-none-any.whl

Download URL pydecide-0.2.19-py3-none-any.whl
Size 67.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9dd1bf1d8896e3fc61db8b2904d76cd5f65a5edfe6649417371378bce49fbb72
BLAKE2b-256 checksum
How to use checksums
4346a42dcb0824e004937b26d61f850b790039277cdd349f2e68abb3215aa2a4
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 21, 2026.

Transparency log

Security for pydecide 0.2.19

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.

Select from 2 files

Attestation for pydecide-0.2.19.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 21, 2026.

Transparency log

A 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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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