Decision Engine (POST /v1/decisions)
Decisions instead of prose — state and questions with fixed options in, probabilities out. Private preview.
Private preview. The contract is stable, the operation is new: limits, prices and model versions may still change during the preview. Changes are announced in the changelog.
The Decision Engine answers questions with a fixed set of options — which team is responsible, how urgent is this, does a human need to review? It generates no text: for every question the model runs exactly one forward pass and we read the probability of each allowed option directly. That is why a decision comes back in milliseconds when warm, where a chat answer takes seconds.
Measured on 2026-09-20 from a machine in Jena, warm, a ticket text with four options:
65 ms per question on sovr-decision-v1, 198 ms on sovr-decision-v2; four
questions in parallel 123 ms and 397 ms respectively. A chat answer to the same
text on the same models takes 5–30 s. These are our measurements, not a guarantee —
they depend on the provider, the load and the length of the state.
Base URL: https://sovrgpt.com/api/v1 · Scope: decisions · Auth: Authorization: Bearer sov_…
Models
| Model | Operated in | Images | Character |
|---|---|---|---|
sovr-decision-v1 (default) | Germany, approved infrastructure (BSI C5) | yes | very fast, very decisive — probabilities almost always near 1.0 or 0 |
sovr-decision-v2 | EU (Ireland/Finland), zero retention by contract | yes | finer-grained probabilities — for threshold logic |
Both versions run on open base models under our control; which version applies
without a model field and where it computes is listed per organisation under
GET /v1/decisions/models. An owner or admin can change it under
Decision Engine → Settings of this organisation.
Request a decision
curl https://sovrgpt.com/api/v1/decisions \
-H "Authorization: Bearer $SOVR_KEY" -H "content-type: application/json" \
-d '{
"preset": "ticket-routing-v1",
"state": "Since this morning I cannot log in. My invoice was also charged twice — please check this urgently."
}'Response (abridged, a real measurement from 2026-09-20 on sovr-decision-v2):
{
"object": "decision",
"id": "dec_3f1c…",
"status": "completed",
"model": { "id": "sovr-decision-v2", "runtime": "…", "operator": "TensorX Ltd", "country": "IE", "via": "platform" },
"preset": { "id": "ticket-routing-v1", "version": "1.0.0" },
"answers": {
"team": {
"type": "choice",
"value": "support",
"probabilities": { "support": 0.6512, "billing": 0.0367, "sales": 0.0044, "unclear": 0.3076 },
"max_probability": 0.6512,
"normalized_entropy": 0.58,
"calibration": { "status": "uncalibrated" },
"abstain": true,
"abstain_reasons": ["review_only_policy", "calibration_unavailable"],
"diagnostics": { "label_mass": 0.9967, "low_label_mass": false, "missing_labels": [] }
},
"urgency": { "type": "score", "value": 3, "expected_index": 3.29, "normalized_score": 0.82, "probabilities": { "0": 0.0002, "1": 0.0003, "2": 0.0017, "3": 0.7042, "4": 0.2936 }, "…": "…" },
"needs_human": { "type": "binary", "value": true, "probability_true": 0.9933, "…": "…" },
"language": { "type": "choice", "value": "de", "probabilities": { "de": 0.9993, "en": 0.0006, "other": 0 }, "…": "…" }
},
"policy": { "mode": "review_only", "threshold": 0.85 },
"usage": { "input_tokens": 860, "output_tokens": 4, "evaluated_questions": 4, "forward_passes": 4 },
"timing_ms": { "total": 412, "inference_max": 397, "inference_sum": 688 }
}Request
| Field | Required | Meaning |
|---|---|---|
state or messages | exactly one | The state to judge: text, a JSON object/array — or a conversation ([{role, content}], text only). Treated as data, never as instructions. |
questions or preset | exactly one | Your own questions (below) or a server-side pack (GET /v1/decisions/presets). |
model | no | sovr-decision-v1 or sovr-decision-v2. Omitted: the organisation's default. |
images | no | Up to 2 images as [{ "data_url": "data:image/png;base64,…" }], ≤ 4 MB each. data: URLs only — the service never fetches remote URLs. |
policy | no | { "mode": "review_only" } (default) or { "mode": "threshold", "threshold": 0.85 }, see “Release”. |
request_id | no | Free-form, echoed back (correlation). |
Question types
{
"team": { "type": "choice", "instructions": "Which team first?", "options": [ { "id": "support", "description": "technical" }, { "id": "billing" }, { "id": "unclear", "description": "cannot be assigned" } ] },
"urgency": { "type": "score", "instructions": "How urgent?", "levels": ["not", "slightly", "normal", "urgent", "immediately"] },
"needs_human": { "type": "binary", "instructions": "Does a human need to review?", "true_description": "yes", "false_description": "no" }
}| Type | Result |
|---|---|
choice — 2–26 options | value = option id with the highest probability, probabilities over all options |
score — 2–10 ordered levels | value = most likely level (0-based), expected_index = Σ p·i, normalized_score = expected/(n−1). A score is not a physical scale. |
binary | value = true/false, probability_true |
Order matters: options and questions are evaluated in source order. A model may react to the order — if you need to rule that out, test with the options swapped.
Limits (preview)
1–16 questions · state ≤ 16,000 characters · question ≤ 1,000 characters · description
≤ 512 characters · body ≤ 64 KiB. Too large → 413. Unknown fields are rejected
(400), not ignored — a tenant_id or tools in the body must not look as if it had
an effect.
What the numbers mean — and what they do not
probabilitiesis the distribution over your options.max_probability: 0.92means the model gives that option 92 % of the mass. It does not mean it is 92 % likely to be correct.calibration.statusis alwaysuncalibratedin the preview.diagnostics.label_masstells you how much probability sat on the allowed options at all. Below 0.5 the model wanted to write something else — the distribution is then an artefact, andlow_label_massistrue.sovr-decision-v1mostly answers 1.0 / 0 / 0 — very decisive, few shades. For “unsure → human review” the domain option (unclear,other) is more reliable there than a percentage threshold.
Release (abstain)
abstain: true means: do not automate unreviewed. The evaluation is complete
nonetheless — it is a recommendation to your workflow, not an action.
review_only(default): every answer carriesabstain: true.threshold:abstaindrops tofalsewhenmax_probability ≥ threshold, the label mass is sound and the chosen option is notunclear/other/unknown. The reasons are always listed inabstain_reasons.
Binding approvals, access rights and actions stay with your application. A typed wrong answer is still a wrong answer.
Errors
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request_error, invalid_json, images_unsupported | invalid body |
| 401 | — | key missing, invalid or revoked |
| 402 | org_spend_limit, user_spend_limit | spending limit reached |
| 403 | missing_scope | key lacks the decisions scope |
| 404 | unknown_preset, model_not_found | |
| 413 | request_too_large | |
| 502 / 504 | upstream_error, upstream_timeout | the runtime did not answer usably/in time — no partial answer |
| 503 | runtime_unavailable | no measured runtime is reachable in this deployment — a different model is never used silently |
Privacy
State, questions and answers are not stored. Billing counts input tokens (plus one
token per question); only these numbers appear in your usage. Images are never fetched:
only data: URLs are accepted. The processing location is part of every response
(model.operator, model.country).
Further endpoints
GET /v1/decisions/presets— server-side question packs with example states.GET /v1/decisions/models— this organisation's versions, default, operator, country, measurements, limits.
A playground with real inference is available when logged in under Decision Engine in the sidebar.