Skip to content

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 ​

toml
[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.

providerEndpointDefault modelDialect
typesafehttps://api.typesafe.ai/v1/systemonejev-latestsystemone
subqhttps://api.subq.ai/v1/systemonesubq-snap-1.1systemone
openrouterhttps://openrouter.ai/api/v1/systemonetypesafe/jev-1.14systemone
openaihttps://api.openai.com/v1/decisionsgpt-6-lunaopenai
ollamahttp://localhost:11434/v1/systemonenimblesystemone
localyou supply base_urlyou supply modelsystemone

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 ​

toml
[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 switch

Notes:

  • Unusable instances are skipped with one warning each — a missing key never breaks startup, and doctor reports what it finds.
  • [decisions] is hot-reloadable: instance edits apply to later decide calls without a restart. Changes to verify, 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_secs at 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:

json
{"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:

  • confidence is 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_probability appears only when a backend supports it (local startlux-decide with allow_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:

PatternWhere it sitsIn UGENT
In-turn judgment callsThe chat model calls decide instead of guessing a classificationdecide tool
Pre-turn intent routingOne cheap choice classifies the request before any expensive calldecision_classify routing detector
Prompt-injection screeningYes/no checks over the inbound message before the model runsdecision_guard routing detector with intent-reject
Verify-in-a-cascadeThe chat model answers; decision checks score is-this-wrong flags; escalate on any flag[decisions.verify] hook
Tool-call arbitrationShould this risky tool run as asked, before it runs[decisions.gate] hook
Confidence-gated actionAct above a threshold, confirm in the middle, escalate belowthresholds 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.

toml
[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.

toml
[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 seeMeaning
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.

Released under the Private Beta License.