One-call decisions: the d1 decision model
Most "should the agent do X?" questions don't need a chat completion. You don't want
prose, you want a typed decision you can branch on: which team to route to, whether
the customer is angry, how urgent a ticket is. SynapCores exposes exactly that as a single
endpoint — POST /v1/systemone — backed by d1, a purpose-built decision model. One
HTTP call in, a structured answer out, zero generated tokens.
Minimum engine: v2.5.0-ce. The endpoint is additive; nothing else changes.
What you get
You send a state (the situation) plus one or more typed questions. For each question you
get back a structured answer:
Question type |
You provide | You get back |
|---|---|---|
choice |
criteria: a map of {option_name: description} |
the chosen option, a probability for every option, and a confidence |
noul |
just instructions (a yes/no question) |
noul: the probability of "yes", in [0, 1] |
score |
criteria: an ordered list of levels, lowest first (2–10) |
score: the expected level (fractional), per-level probabilities, a legend, and a confidence |
Because it answers by weighing the options rather than writing text, a decision returns in
a single forward pass and reports output_tokens: 0.
Step 1 — Install d1 (it is not bundled)
d1 is pull-on-demand, not shipped inside the engine image — so a fresh install does not
have it until you pull it once. Pull it with the pull subcommand (it streams the GGUF from
registry.ollama.ai into your configured data_dir):
# ~2.7 GiB, one-time. Point --config at the same gateway.toml your server uses,
# so the model lands in that server's data_dir.
synapcores --config /etc/synapcores/gateway.toml pull d1:latest
pulling library/d1:latest
verifying sha256... ok
writing manifest sidecar... ok
installed library/d1:latest (architecture=lfm2, size=2.7 GiB, digest=sha256:fe008a6acf70)
Confirm it is present:
synapcores --config /etc/synapcores/gateway.toml models list
NAME ARCH SIZE PULLED
library/all-minilm:latest bert 43.8 MiB ...
library/d1:latest lfm2 2.7 GiB ...
library/qwen2.5:7b qwen2 4.4 GiB ...
The pull is idempotent and resumable: re-running it when the model is already present re-verifies the checksum and returns without re-downloading, and an interrupted pull resumes from where it stopped.
Step 2 — Enable it
The decision endpoint does not have its own model setting — it shares
[query.ai_service].model, which is your engine's text-generation model (the one
GENERATE, chat, and AGENT_RUN use). So the rule is simple:
Leave
[query.ai_service].modelas your chat/generation model, and named1:latestexplicitly on every/v1/systemonecall. That is the only setup that keeps both paths working — decisions on d1, generation on your chat model.
A typical config — the generation model stays put, and auto_pull lets the first decision
call fetch d1 on demand:
[query.ai_service]
provider = "local"
model = "qwen2.5:7b" # your chat / GENERATE / AGENT_RUN model — leave it as-is
auto_pull = true # a /v1/systemone call naming d1 pulls it on first use if missing
Why not just set model = "d1:latest" here? Because that key is the generation
default, not a decision default. Pointing it at d1 also repoints GENERATE, chat, and
AGENT_RUN at a decision model that returns zero generated tokens — your text
generation would stop producing text. d1 is selected per request, never by swapping
your generation model.
How /v1/systemone resolves which model to run, in order:
- the
"model"field in the request body — set this to"d1:latest"for a calibrated decision; - otherwise
[query.ai_service].model— your generation model, which is not calibrated for decisions; - otherwise a built-in
d1:latestfallback (reached only when no generation model is configured at all).
On any real install step 2 is your chat model, so step 1 is the one to rely on: name the decision model explicitly in the request (Step 3). No restart is needed to use a model that is already in the store.
Dedicated decision instance (advanced). If an engine is deployed only to serve
/v1/systemoneand never runsGENERATE/ chat /AGENT_RUN, you may setmodel = "d1:latest"under[query.ai_service]so callers can omit the field. Do this only when you are certain no text-generation traffic reaches that instance — otherwise keep the per-request pattern above.