Backends
A backend is the thing that actually answers. SystemOne owns one and exposes
it as agent.backend; which one you get is the config you pass, or
SYSTEM_ONE_BACKEND when you pass none.
| Backend | Extra | What it does |
|---|---|---|
http (default) |
system-one[http] |
Calls a hosted provider over HTTP. |
onnx |
system-one[onnx] |
Runs the model in-process. No network, no API key. |
stub |
none | Answers at random. For wiring tests and demos only. |
Vendor imports live inside the backend factory, so the extras stay genuinely
optional: installing the core package pulls neither httpx2 nor onnxruntime.
Selecting a backend whose extra is missing raises ImportError with the pip
command that fixes it.
The protocol
Every backend satisfies the same one-method protocol —
Backend, or
AsyncBackend for the awaited version:
That is the whole contract. Anything implementing it can be handed to an agent.
HTTP
One class per vendor, each an HTTPConfig with its endpoint filled in:
SystemOne(TypesafeConfig()) # https://api.typesafe.ai/v1/systemone
SystemOne(OpenRouterConfig()) # https://openrouter.ai/api/alpha/decisions
SystemOne(HTTPConfig(base_url="https://my-host", model="mine"))
The request goes to base_url + path. path defaults to /v1/systemone, and
you can point it anywhere with path= or SYSTEM_ONE_PATH:
The presets require SYSTEM_ONE_API_KEY (or api_key=); a plain HTTPConfig
sends no Authorization header without one. Every field also reads its
SYSTEM_ONE_* variable, so SystemOne() alone needs SYSTEM_ONE_BASE_URL and
SYSTEM_ONE_MODEL.
Retries are built in: 408, 429 and 5xx are retried up to
SYSTEM_ONE_MAX_RETRIES times with exponential backoff, honouring the
Retry-After header when the server sends one. 401 and 403 are never
retried — see Errors.
Backend-specific calls stay reachable through agent.backend.
Bringing your own client
The HTTP backends take an optional client= — an httpx2.Client, or an
httpx2.AsyncClient for AsyncHTTPBackend. Without one they build their own
with timeout=SYSTEM_ONE_TIMEOUT. Pass one to share a connection pool or to
control its lifetime; closing it is up to you:
with httpx2.Client(timeout=30) as client:
agent = SystemOne(using=HTTPBackend(TypesafeConfig(), client=client))
agent.ask(...)
Fastino GLiDE
GLiDE is Fastino's hosted
decision model. It answers the same POST /v1/systemone form, so a plain
HTTPConfig is all it takes. Create a key at
agent.fastino.ai (Settings → API keys):
import os
from pydantic import SecretStr
agent = SystemOne(
HTTPConfig(
base_url="https://api.fastino.ai",
model="fastino/GLiDE",
api_key=SecretStr(os.environ["FASTINO_API_KEY"]),
timeout=300,
)
)
response = agent.ask(
"Refund request: 10 days old, 30-day window applies.",
{"refund_allowed": {"type": "noul", "instructions": "Does this qualify for refund?"}},
)
print(response.nouls["refund_allowed"].noul)
The same thing from the environment, with no config object:
SYSTEM_ONE_BASE_URL=https://api.fastino.ai
SYSTEM_ONE_MODEL=fastino/GLiDE
SYSTEM_ONE_API_KEY=<your Fastino key>
SYSTEM_ONE_TIMEOUT=300
Things to know:
- Auth. The key goes out as
Authorization: Bearer …; Fastino accepts that as well as its nativeX-API-Keyheader. - Cold starts. An idle or newly deployed model can take a while to answer.
Fastino recommends a read timeout of at least 300 seconds, hence
timeout=300. A425(deployment warming) is not in the SDK's retry set — catchAPIError, wait about 60 seconds and ask again.429and5xxare retried as usual. - Limits. Each rendered prompt (state plus one question) must fit in
40,000 tokens, and a
choiceorscoretakes at most 255 options or levels. No streaming, and one state per request. - Thresholds. Probabilities are calibrated per model, not per business —
tune the cut-off on representative data instead of assuming
0.5. - Not covered.
POST /v1/chat/completions(GLiNER extraction, decoder LLMs) is a different contract and is not part of this SDK.
A local server that speaks the form
HTTPConfig is not tied to a hosted vendor: any server that answers a POST
in the same request and answer shape works, including one on the machine
itself. It can live on any route; set path to match. coreai-kit
runs a catalog model on a Mac (Apple's Core AI runtime, macOS 27) behind the
default /v1/systemone, so it needs no path:
No key, nothing leaves the machine. One difference from the hosted API to know
about: a choice there lists at most 16 options, and a longer list comes back as
a 422 with the reason.
kev
kev is a family of small decision models
you train and serve yourself, a drop-in for hosted jev. Its server answers on
the default /v1/systemone, so it needs no path:
git clone https://github.com/jaredpalmer/kev.git && cd kev
uv sync --extra serve
uv run --extra serve python -m kev.serve --run jaredpalmer/kev-4b --port 8009
If the server was started with KEV_API_KEY, pass the same value as
api_key (or SYSTEM_ONE_API_KEY); it is sent as a bearer token. For a
deployed instance (e.g. Modal), point base_url at its URL.
ONNX
or SYSTEM_ONE_BACKEND=onnx with SYSTEM_ONE_ONNX_DIR / SYSTEM_ONE_MODEL.
The directory must hold three things for the configured model:
onnx/
├── laya.onnx # the exported graph
├── laya.json # model config: prompt layout, temperature buckets
└── tokenizer/
└── tokenizer.json
Sessions are cached per (directory, model) pair, so a second agent — or a
second model on the same agent — does not reload gigabytes of weights. A
missing graph raises SystemOneError naming the path it looked for.
Those artifacts are produced once from a laya checkpoint — see Running a local model.
The async ONNX backend runs the same synchronous session off the event loop, so
await agent.ask(...) does not block it.
Stub
or SYSTEM_ONE_BACKEND=stub. Every question gets a uniformly random answer of the
right shape — a noul probability, a choice drawn from its labels with a
distribution, an expected score with its legend — and zero usage. Nothing about
the state is read, so never use it for decisions.
Injecting your own
Pass any object satisfying the protocol as using=. Tests use this to avoid the
network entirely:
class FakeBackend:
def ask(self, request):
return SystemOneOutput(
model=request.model,
usage=Usage(input_tokens=0, output_tokens=0),
answers={"urgent": NoulAnswer(noul=0.9)},
)
agent = SystemOne(using=FakeBackend())
An injected backend owns its model: ask falls back to backend.model, not
SYSTEM_ONE_MODEL.