Typed decisions (choice/noul/score) and question-driven content ranking (decision rank) with a pluggable provider. aux4/decision ships with bm25 built in — a free, offline, deterministic lexical ranker with no key, no network, and no model. Install it and decision ask/decision run/decision rank work immediately.
Install aux4/decision-jev for calibrated probabilities, aux4/decision-openai for model-estimated probabilities from the OpenAI Decisions API, or aux4/decision-gemma for local Gemma decisions. The generic ask and run commands keep the same shape; select a provider with --provider.
aux4 aux4 pkger install aux4/decision
aux4 decision ask "Which category fits best?" --type choice \
--options "bug=a defect report" \
--options "feature=a feature request" \
--state "App crashes on launch after the last update"
bug
No key, no network, no --model. bm25 treats each label's description as a tiny document and --state as the query, ranking by Okapi BM25. --type defaults to noul (a calibrated yes/no probability, matching aux4/decision-jev's own default), which bm25 does not support — pass --type choice explicitly to use the built-in provider.
| Provider | --provider | Cost/network | Supports | Scale | |---|---|---|---|---| | bm25 (built in) | bm25 (default) | free, offline | choice only | relative (uncalibrated) | | jev (aux4/decision-jev) | jev | paid API call | choice, noul, score | probability (calibrated) | | OpenAI Decisions (aux4/decision-openai) | openai | paid API call | choice, noul, score | estimate (model-estimated probability) | | Gemma (aux4/decision-gemma) | gemma | local models; first embedding use downloads a model | choice, noul | estimate (uncalibrated similarity/model score) |
A provider declares which question types it supports. Asking bm25 for --type noul or --type score is a clear, immediate error naming the provider and the type — bm25 has no corpus to rank a yes/no or a rubric question against, so it refuses rather than guessing or silently returning a default answer. Install the provider package that supports the requested type and pass its matching --provider.
There is no automatic fallback between providers. If you want jev's answer with a bm25 backstop, call each provider explicitly per request — never assume one silently substitutes for the other, especially anywhere a threshold gates a real decision.
scale field — understand what each score meansEvery response — from any provider — declares two fields: provider and scale.
"scale": "probability" — jev's calibrated probability scale."scale": "estimate" — an OpenAI or local Gemma model estimate. Do not assume it is calibrated; choosethresholds using labeled examples and the costs of false positives and false negatives. Gemma choice scores are softmax-normalized similarities, while noul scores use local token logprobs.
"scale": "relative" — an uncalibrated ranking (bm25). The numbers only mean "higher is more relevantthan lower, in this response" — they are not probabilities and must never be compared against a fixed threshold.
--threshold is available for noul with jev, OpenAI, or Gemma. OpenAI and Gemma gate on estimates, not guaranteed-calibrated probabilities. bm25 (scale relative) rejects --threshold; use --top N instead.
choice — pick one label out of a candidate list (--options). Returns the chosen label; the fullresponse also carries a scores map for every label, so one call reranks an entire candidate list. Supported by every provider.
noul — a single yes/no estimate. Returns a bare number, not a boolean — there is no true/falsefield. Supported by jev, OpenAI, and Gemma; use --threshold to turn it into an exit-code guard.
score — pick a point on an ordered rubric (--levels, minimum 2). Returns a weighted score acrossthe rubric. Supported by jev and OpenAI.
decision ask — one question, one lineThe one-liner form: ask a single question, get the bare answer on stdout, so it drops straight into ${} in an execute chain.
aux4 decision ask "Which category fits best?" \
--type choice \
--options "bug=a defect report" \
--options "feature=a feature request" \
--state "App crashes on launch after the last update"
bug
Pass several samples with repeated --state flags. A single sample still prints the bare answer; multiple samples print one answer per line in input order. API-backed providers make one request per sample; Gemma batches choice embeddings across the samples in one local call. With --top, each sample's ranking gets a sample N: header.
aux4 decision ask "Is this message spam?" --provider openai --type choice \
--options spam --options not-spam \
--state "Buy now, limited offer" \
--state "Can we move our meeting to 2?"
The same variables can come from config.yaml:
config:
spam:
provider: openai
type: choice
question: Is this message spam?
options:
- spam
- not-spam
state:
- "Buy now, limited offer"
- "Can we move our meeting to 2?"
aux4 decision ask --config spam
See the top N candidates instead of just the winner:
aux4 decision ask "Which category fits best?" --type choice --top 2 \
--options "bug=a defect report" \
--options "feature=a feature request" \
--options "docs=a documentation request" \
--state "App crashes on launch after the last update"
bug 2.6995
feature 0.0000
Note: if no --state is supplied, stdin is read as one sample. --type defaults to noul, so bm25 callers must always pass --type choice explicitly:
cat ticket.txt | aux4 decision ask "Which category fits?" --type choice --options bug --options feature
noul guard (requires aux4/decision-jev)noul returns a bare probability of "yes" — never a boolean. --threshold is what turns it into an exit-code guard: the command exits 0 when the probability is at or above the threshold, 1 when below it. Always set the threshold explicitly for anything that gates a pipeline step.
aux4 decision ask "Is this a security issue?" --type noul --provider jev \
--state "$(cat report.txt)" --model <model-id> --threshold 0.7 \
&& aux4 notify-security-team || echo "not a security issue"
decision run — a reusable, versionable question setrun asks a full set of named questions against one or more states, and prints the complete JSON response — provider, scale, an answers map (with scores for choice), and token usage — so it's the form to reach for when you want to keep the question set under version control instead of retyping flags.
Configuration file (bm25, no key needed):
config:
triage:
questions:
category:
type: choice
instructions: Which category best fits this ticket?
options:
bug: a defect report
feature: a feature request
question: a question, not an issue
state:
- "App crashes on launch after the last update"
aux4 decision run --config triage
{
"provider": "bm25",
"scale": "relative",
"answers": {
"category": {
"type": "choice",
"choice": "bug",
"scores": {
"bug": 1.9977167649325933,
"feature": 0,
"question": 0
}
}
},
"usage": {
"input_tokens": 0,
"output_tokens": 0
}
}
run also accepts the question set as a raw JSON object via --questions '{...}' instead of a config section. Repeat --state for multiple samples, or provide a YAML array under state; if neither is provided, stdin is treated as one sample. Each sample is processed in its own provider request. Multi-sample output is a JSON object with ordered results, each containing its one-based index and normal response. --top N limits each choice answer's scores map to its top N labels — useful when a candidate list is large.
Note: a question's label map/list is always named options (choice) or levels (score) here, matching decision ask's flags — not criteria, which is the field name the jev / TypeSafe System One API itself uses on the wire. A question set copied straight from jev's vendor docs will need criteria renamed to options or levels.
Use the same command variables in a config section for a one-question ask; for example, define provider, question, type, options, and state, then run aux4 decision ask --config spam. Omit state in that section to read one sample from stdin.
Uses the standard aux4/config conventions: --configFile to point at a specific file (default: auto-discovered config.yaml/config.yml/config.json in the current directory) and --config <section> to select which section's values populate provider, model, questions, and state.
If any question in the set has a type the resolved provider doesn't support, the whole call fails with a clear error naming the question and the type — never a partial response with some questions silently skipped.
decision rank — find the blocks of text that answer a questiondecision rank takes a question and a set of candidate blocks of text — a web page, a document, search results — and returns the blocks that answer it, best first, verbatim, with their scores and any metadata you attached (url, ref, heading, ...). Nothing is generated or rewritten: every returned text is exactly one of the inputs.
aux4 decision rank "What is the refund policy?" --top 2 < terms.md
{
"provider": "bm25",
"scale": "relative",
"question": "What is the refund policy?",
"answered": null,
"blocks": [
{
"id": "b14",
"score": 6.8529,
"text": "**Payment Based on Plan** ... There will be no refunds or credits for partial months of service.",
"heading": "Payment > Billing Schedule; No Refunds",
"offset": 4870
},
{
"id": "b13",
"score": 6.6073,
"text": "### [Billing Schedule; No Refunds](#billing)",
"heading": "Payment > Billing Schedule; No Refunds",
"offset": 4799,
"url": "#billing"
}
],
"candidates": 304,
"skipped": 2,
"windows": 0,
"usage": {
"input_tokens": 0,
"output_tokens": 0
}
}
--blocks — a JSON array. Each item is a string, or an object with text plus an optional id and anyother fields, which are passed through to the output untouched (ids default to b0, b1, ...).
--text — plain text or markdown, split into blocks by --split:auto (default) — markdown-aware: every heading, list item and table row is its own block, other linesgroup into blank-line paragraphs, a fenced code block is one block.
heading — one block per section (a heading and everything under it).paragraph — blank-line separated paragraphs.line — every non-empty line.Text blocks carry offset (character position in the input), heading (the heading path they sit under, e.g. Payment > Billing) and url (the first link in the block). Spans longer than --maxBlockChars (default 1500) are chunked at sentence boundaries, so nothing is lost.
Blocks that are machine data rather than prose — an embedded JSON payload (such as a page's __NEXT_DATA__), a script or style body, minified or base64 text — are skipped and counted in skipped. They never answer a question and can be large enough to swamp a paid provider's context.
--granularity sentence splits every block into sentences and ranks those instead. Each sentence keeps its parent's metadata and adds block (the parent id), sentence (its index in the parent) and sentenceOffset (its character position in the parent's text); its id is <block>.s<n>. A common two-step pattern is to rank blocks first, then rank the sentences of the top few blocks to pinpoint the exact answer.
refund matches refunds and refundable,pricing matches price. Free, offline and instant. Blocks that share no word with the question are not returned. scale is relative, answered is null, and --threshold is rejected (use --top). bm25 only matches words: a question like "what are the latest articles?" will not find headlines that never use the word "article".
--provider jev, requires aux4/decision-jev) — asks a calibrated yes/no question of every block("does this block answer the question?") plus one for the whole set, so score is a probability, and answered is the probability that any block answers the question at all. --threshold 0.5 returns only blocks at or above it. Inputs with more than --shortlist blocks (default 150) are bm25-prefiltered to that many first.
Gemma supports decision ask and decision run; it does not support decision rank.
| Flag | Default | Description | |---|---|---| | --blocks | | JSON array of blocks | | --text | | text / markdown to split | | --split | auto | auto, heading, paragraph, line | | --granularity | block | block or sentence | | --top | 5 | number of blocks to return (0 = all) | | --threshold | | cutoff for noul with jev probability or OpenAI estimate | | --provider | bm25 | bm25 or jev | | --shortlist | 150 | bm25 prefilter size before a plugin provider | | --maxBlockChars | 1500 | longest text block; a plugin also truncates longer blocks to this before sending | | --model, --apiKey, --baseUrl, --brokerUrl, --brokerToken | | passed to the plugin provider |
Note: for large pages pass the input on stdin rather than as --blocks/--text: a single command-line argument is limited to about 128KB on Linux.
aux4 aux4 pkger install aux4/decision-jev
Then pass --provider jev (and --model <model-id> for ask/run) to any of the three commands. See the aux4/decision-jev README for authentication and model configuration — core never needs a key and works exactly the same with or without the plugin installed.