Decision Models
Decision models answer typed questions about some text — a yes/no probability, a choice with per-option probabilities, or a score on ordered levels — in a single forward pass, without generating text. They are not chat models: no prose, no reasoning trace, just calibrated probabilities, typically in a few hundred milliseconds and at a fraction of a chat model's price (roughly $0.04 per million input tokens, output free, on every major backend).
UGENT speaks both wire dialects of this contract from one configuration: the TypeSafe-style systemone dialect (POST /v1/systemone) and OpenAI's decisions dialect (POST /v1/decisions). One [decisions] section covers cloud providers (TypeSafe Jev, SubQ Snap, OpenAI Decisions, OpenRouter's mirror) and local runtimes (LM Studio, llama.cpp, startlux-decide, Ollama, vLLM).
Everything is off until you configure an instance: no section, no decide tool, no calls.
Quick Start
[decisions]
default_instance = "jev"
[decisions.instances.jev]
provider = "typesafe"
api_key = "$TYPESAFE_API_KEY"
[decisions.instances.subq]
provider = "subq"
api_key = "$SUBQ_API_KEY"
[decisions.instances.openai]
provider = "openai"
api_key = "$OPENAI_API_KEY"With one usable instance, UGENT registers a decide tool. Ask your agent to use it:
Use the decide tool: is "refund my charge, it posted twice" an urgent billing matter? One noul and one choice question, one call.
and you get back:
team: choice=billing p=0.9943 confidence=0.9915
urgent: noul=0.7104
[instance=jev model=jev-1.14.0 input_tokens=251]Providers and Presets
The provider field fills the endpoint's base URL, request path, model, and dialect; explicit fields always win.
provider | Endpoint | Default model | Dialect |
|---|---|---|---|
typesafe | https://api.typesafe.ai/v1/systemone | jev-latest | systemone |
subq | https://api.subq.ai/v1/systemone | subq-snap-1.1 | systemone |
openrouter | https://openrouter.ai/api/v1/systemone | typesafe/jev-1.14 | systemone |
openai | https://api.openai.com/v1/decisions | gpt-6-luna | openai |
ollama | http://localhost:11434/v1/systemone | nimble | systemone |
local | you supply base_url | you supply model | systemone |
Local endpoints verified with UGENT: startlux-decide on http://127.0.0.1:8090 (set allow_none = true for its none_probability extension), LM Studio on http://localhost:1234 (0.4.26+ serves /v1/systemone for models it recognizes as decision models), llama-server on :8081, and Ollama on :11434.
Through the secret vault, store keys once and reference them by handle: api_key_ref = "typesafe_api_key" takes priority over api_key. Vault-backed keys resolve with redaction and an audit row per read.
Configuration Reference
[decisions.instances.<name>]
provider = "typesafe" # preset: typesafe | subq | openrouter | openai | ollama | local
dialect = "systemone" # override the preset's dialect (systemone | openai)
base_url = "https://..." # override the preset's base URL; required for local
path = "/v1/systemone" # override the preset's request path
api_key = "$TYPESAFE_API_KEY" # $ENV_VAR or literal
api_key_ref = "typesafe_api_key" # vault handle, takes priority over api_key
model = "jev-latest" # override the preset's model
timeout_secs = 30 # request timeout
max_retries = 2 # retries for transient failures (408/429/5xx/529)
retry_status_codes = [] # extra status codes treated as retryable
long_context = false # TypeSafe only: 64k -> 262k tokens (slower, less accurate)
allow_none = false # startlux-decide: request a none_probability on choice answers
input_price_per_mtok = 0.042 # cost estimate when the backend reports no cost
enabled = true # per-instance switchNotes:
- Unusable instances are skipped with one warning each — a missing key never breaks startup, and
doctorreports what it finds. [decisions]is hot-reloadable: instance edits apply to laterdecidecalls without a restart. Changes toverify,gate, or which instances can build trigger a full agent rebuild at the next config reload, because hooks and tool registration happen at construction.- Keep
timeout_secsat or below 4 for instances the verify/gate hooks use: hooks run under a 5-second executor budget, and a slower endpoint disarms its hook (fail-open, one warning). - Extra headers go under
[decisions.instances.<name>.headers], values$ENV_VAR, vault handle, or literal — useful for OpenAI organization/project scoping (OpenAI-Organization,OpenAI-Project).
The decide Tool
Questions are passed as an array and answered in one call — batching independent questions is the point (the fan-out pattern): extra parallel questions cost roughly nothing on every backend.
- noul —
{name, type: "noul", instructions?, if_true?, if_false?}: the answer is a probability in 0..1 that the condition holds. - choice —
{name, type: "choice", instructions?, options: [{name, description?}]}: the answer names the top option with per-option probabilities and a confidence. - score —
{name, type: "score", instructions?, levels: ["lowest", ..., "highest"]}: the answer is the probability-weighted level and can fall between levels.
Example call and answer:
{"state": "Customer says: I was charged twice for my order.",
"questions": [
{"name": "team", "type": "choice",
"instructions": "Which department should handle this?",
"options": [{"name": "billing", "description": "Payments and refunds"},
{"name": "technical", "description": "Application problems"}]},
{"name": "urgent", "type": "noul",
"instructions": "Does this require urgent attention?"}]}team: choice=billing p=0.9943 confidence=0.9915 none_probability=0.0029
urgent: noul=0.7104
[instance=jev model=jev-1.14.0 input_tokens=251]Reading the numbers:
confidenceis concentration, not correctness — how peaked the probability distribution is. Thresholds belong to you, calibrated on your traffic; common starting points are 0.6 for low-stakes auto-action and 0.85-0.9 for irreversible ones.none_probabilityappears only when a backend supports it (local startlux-decide withallow_none). It is an alarm score, not a calibrated probability: at 0.2 or above, suspect that no listed option fits. The portable alternative is a companion noul — "does the input name any of our products?" — read alongside the choice.- Limits enforced per call: 1-64 questions, option names 1-100 chars of letters, digits, dot, underscore, hyphen. The 255-option ceiling is the hosted backends' cap: local runtimes are stricter (Ollama and vLLM take at most 26 options; llama.cpp's cap is per model), so keep choice lists to 26 when an instance may target one of them.
Use the tool for classification, routing, arbitration, verification checks, yes/no gates, and ratings. Do not use it for generation, explanation, or custom-JSON extraction — those belong to the chat model and its structured output.
Decision Models with the Chat Model
Where a decision model sits relative to your chat model, and what UGENT ships for each pattern:
| Pattern | Where it sits | In UGENT |
|---|---|---|
| In-turn judgment calls | The chat model calls decide instead of guessing a classification | decide tool |
| Pre-turn intent routing | One cheap choice classifies the request before any expensive call | decision_classify routing detector |
| Prompt-injection screening | Yes/no checks over the inbound message before the model runs | decision_guard routing detector with intent-reject |
| Verify-in-a-cascade | The chat model answers; decision checks score is-this-wrong flags; escalate on any flag | [decisions.verify] hook |
| Tool-call arbitration | Should this risky tool run as asked, before it runs | [decisions.gate] hook |
| Confidence-gated action | Act above a threshold, confirm in the middle, escalate below | thresholds in your recipes; the tool keeps raw probabilities |
Post-Turn Verification ([decisions.verify])
After a turn, one decision call scores narrow is-this-wrong checks over the finished answer judged against the request that prompted it (frame them so true means wrong; one judgment per check; any flag firing is the verdict — aggregate with max, not mean). The hook annotates the turn's metadata and logs a warning when the gate trips. It annotates only: escalation stays with your fallback_instances setup.
[decisions.verify]
enabled = true
instance = "jev"
threshold = 0.7
[decisions.verify.checks]
hallucinated = "The answer states facts absent from the request or tool results"
off_target = "The answer does not address what was asked"Pre-Tool Gating ([decisions.gate])
Before a tool on the list runs, one decision call asks whether executing it as asked would be wrong; a check at or above threshold blocks the call with a reason naming the flags. The gate can only add restrictions: it never lowers a confirmation bar, the security validator still runs for everything it lets through, and it fails open (one warning) when the endpoint errors.
[decisions.gate]
enabled = true
instance = "subq"
threshold = 0.7
tools = ["bash"]
[decisions.gate.checks]
unsafe = "Executing this call as asked would be destructive or against the user's intent"Both hooks are opt-in and off by default. Routing recipes for the two detectors — including the prompt-injection screen — are in Routing.
Failure Modes
| What you see | Meaning |
|---|---|
insufficient credits (402) | SubQ or OpenRouter account is out of credits; terminal, not retried |
rate limited / overloaded (529) | Retried with backoff honoring Retry-After (capped at 60 s), then surfaced |
not a decision model (501) | The endpoint is not serving decisions for that model — a chat GGUF behind a local /v1/systemone answers exactly this |
refused question (OpenAI dialect) | The model refused one question; the whole call errors rather than guessing |
Validation (400/422) | The provider's message is shown: too many options, oversized state, malformed question |
Privacy and Cost
Decision calls are egress: the state you send leaves the machine to the configured provider, exactly like a chat turn — and the verify hook ships whole answers, the gate hook ships tool arguments plus the user's message. Retention as of October 2026: SubQ keeps decision inputs and outputs up to 60 days (organization-level zero retention on request); TypeSafe is zero-retention for enterprise customers; OpenAI retains up to 30 days for abuse monitoring with organization-level opt-out.
Costs are input-only everywhere. When the backend reports a cost (OpenRouter does), UGENT records it; otherwise input_price_per_mtok produces an estimate. A two-question triage call runs about 150-350 input tokens — on the order of $0.00001 per call.