Decisions
jevlin decide asks a decision model typed questions about a state, where the router offers it. A router without decisions answers decisions_not_supported. The agents' skill does not teach it yet.
jevlin decide --stdin takes one JSON object and answers with one envelope carrying the same header as a search, with command set to decide. It sends POST /v1/decisions to the configured router with the same key, deadline, redirect policy and user agent as a search, and nothing about it is kept on this machine.
A decision is billed, and never sent twice. result.usage.cost_micros says what it cost, and it is one request the client never repeats.
The request
Version 1:
| field | value |
|---|---|
version | 1, required. |
model | Required: vendor/model, as jevlin decide --models lists it. |
state | Required and not null: what the questions are about, a string, object or array. |
questions | Required: a non-empty object of question name to question, each an object, such as {"type": "noul", "instructions": "…"}. |
options | An object of the vendor's options. |
sources | 1 to 5 entries, each exactly {"source_id": "s_…"}, a page's source_id from a search's result.merged. |
max_chars | 1000 to 50000. |
A malformed value, and any other field, answers fix_input before the router is called, with a code naming the field: unsupported_version, unknown_field, missing_model, invalid_model, missing_state, missing_questions, invalid_questions, invalid_options, invalid_sources or invalid_max_chars. A request over 1 MiB on stdin is input_too_large. Nothing else is checked here: the router judges the state, each question and the options, and the client sends them byte for byte, spacing and key order included. A router that does not take sources and max_chars yet refuses a request carrying them as malformed_json.
The answer
On success, result has:
| field | meaning |
|---|---|
request_id | The router's id for this decision, also at the envelope's top level. |
model, model_resolved | The model as asked for, and what the vendor echoed, when it did. |
answers | One entry per question name, the router's object for it: type is noul (with noul), choice (choice, probabilities), score (score, probabilities, legend) or refusal, and confidence when the vendor reports one. Only the whitespace between tokens is removed. |
citations | The sources the vendor reports for the whole request: url, title, excerpts. Empty when none. |
sources | The pages the router read for a request carrying sources: source_id, url, title, chars, cached, cost_micros. |
usage | input_tokens, output_tokens, grounded_calls, cost_micros, pages_cost_micros, cost_source (modeled, reported or free), byok and latency_ms. |
A refusal is an answer, not a failure: ok stays true and the other answers stand. A 2xx with no answers is invalid_router_response, exit 4.
Listing the models
jevlin decide --models sends GET /v1/decisions/models with the same key and reads nothing from stdin. result.models is the router's list, each with id (the value model takes), vendor, vendor_model, cost in millionths of a dollar (per_request_micros, per_prompt_ktok_micros, per_search_call_micros, a zero left out), free (requests_per_day, absent with no free tier), grounded and available (whether this account can call it). Listing spends nothing. --stdin and --models together are a usage error.
When it fails
| router answer | exit | code | retryable | action |
|---|---|---|---|---|
400 unknown_model | 3 | unknown_model | false | fix_input: jevlin decide --models lists them |
400 invalid_request, malformed_json | 3 | the router's | false | fix_input |
400 upstream_rejected | 3 | upstream_rejected | false | fix_input: error.message is the vendor's |
413 payload_too_large | 3 | payload_too_large | false | fix_input |
403 decisions_unavailable | 3 | decisions_unavailable | false | connect: this account cannot call this model, and error.message says why |
| 404 or 405 | 3 | decisions_not_supported | false | report: this router has no decisions |
503 decisions_disabled | 3 | decisions_disabled | false | report: this router has decisions switched off |
503 model_unavailable | 4 | model_unavailable | false | report: the platform holds no working key for this model |
503 upstream_failed | 4 | upstream_failed | the body's retryable | retry when true, with retry_after_ms if the router sent one; report otherwise |
402 search_credit_only | 3 | search_credit_only | false | report: the credit left this week is reserved for search; decisions resume at the weekly reset or after a top-up |
| any other status | as for a search |
Exit codes are as for a search: 0 for a valid answer, 1 for transport, timeout or cancellation, 2 for usage, 3 for a 4xx, for no usable key, or for a router that has decisions switched off; 4 for any other 5xx or an answer the client could not use.
See also
- The envelope: the header every answer shares, and the failure codes.
- The MCP server: the same request as the
decidetool, and--modelsasdecide_models. - Reading a page in full: where a
source_idcomes from, and what reading a page costs.