# Decision Model

A fast classifier model, served by TypeSafe or OpenRouter, that answers typed questions for dispatch, watch checks and content filtering.

The decision model (`internal/agents/decision`) is not a chat model: it receives a JSON state plus a set of multiple-choice questions and returns one choice per question with a confidence. It has two providers, `typesafe` and `openrouter`. The choices are listed live: TypeSafe from its own models API, OpenRouter from the models that report a `decisions` output modality, minus five OpenRouter models Agenvoy excludes because their input format differs. Each provider's list is cached for 15 minutes (`TTL_MODELS_CACHE_SEC`) and dropped when that provider's key is saved, its OAuth completes or its login is cleared. The selection is stored as `decision_model` in `config.json` as `<provider>@<model>`; empty or `off` means off.

Set it up in two steps. First store a key: TypeSafe is a row in the **API Key** tab of `/model add` (`o` opens `https://console.typesafe.ai/keys`); saving its key stores `TYPESAFE_API_KEY` and stops there, with no model list to pick from. OpenRouter uses the same `OPENROUTER_API_KEY` as its chat models. Over HTTP, `POST /v1/provider/typesafe/key` stores the key. Then pick the model in `/model` → `decision` (`disable` is listed first and clears it), in the web model settings under **Decision**, or with `POST /v1/model` `{decision}`; a value not in `decision_options` returns 400.

Each call has 3 seconds (`TIMEOUT_DECISION_CALL`). The request text is cut at 2,048 runes (`MAX_DECISION_RUNES`); questions over many items are split into batches of at most 120 questions (`MAX_DECISION_QUESTIONS`) and about 20,000 runes of state (`MAX_DECISION_STATE_RUNES`). Except for the skill filter, a judgment takes effect only when its confidence is at least 0.7 (`DECISION_MIN_CONFIDENCE`, watch checks included); a failed or timed-out call leaves the content as it was.

## What uses it

| Use | What the decision model decides |
|---|---|
| Dispatch | Work kind, a model named in the request, and whether the subject continues (see below) |
| Watch checks | Whether `background_command` output flagged by an error keyword shows a real error, and whether it only repeats the last report; a benign or repeated hit is not reported, and a failed call reports it unchecked with a note. Without a decision model every hit is reported. The interval/prompt watch of `run_command_background` and `run_websocket` was Removed in v1.3.2 |
| `search_web` | Drops results that do not bear on the request; when none is left, the tool says so and asks for a better query |
| `fetch_page` | Removes noise blocks from a fetched page before it is cached, and withholds a page that does not bear on the request |
| `read_files` / `chat_history` | Since v1.3.11, withholds a file read by `read_files` or a task read by `chat_history` `mode=read` when it does not bear on the request, returning a note in its place. Only content of at least 4,096 runes is checked, a `read_files` entry with `around` is never checked, and the same file or task is withheld at most once per session, so a second read returns the content |
| Turn context | Adds a tool-gap hint pointing to `reasoning_guide(topics=["tool_generate"])` when no tool covers the request, and hides skills it does not judge relevant from the skill list |
| Session summary | Drops noise exchanges and outdated `key_decisions` / `past_discussions` entries before the summary is merged; since v1.3.7 it also keeps only the `key_decisions` / `past_discussions` / `current_discussion` entries relevant to the request (general rules on language, format or workflow count as relevant) when the summary is injected into the turn context, keeping every entry when the call fails and leaving the summary out when none is relevant |
| Context overflow | Before the oldest-first trim, prunes tool steps the request no longer needs |

## Dispatch

Whenever a decision model is set, each routed request runs the decision call and the LLM dispatcher at the same time; the first valid answer is used, and when the decision call fails the LLM dispatcher's answer is used. With no decision model set (`decision_model` empty or `off`), only the LLM dispatcher runs. There is no separate switch: `dispatcher_beta` (config field `DispatcherDecision`), the **Decision Model** entry of `/model` → `dispatch` and of the web Dispatcher list, and `POST /v1/model` `{dispatcher_beta}` were Removed in v1.3.2; set or clear the decision model instead, and a leftover `dispatcher_beta` in `config.json` is ignored.

The call asks up to three questions: what kind of work the request is, whether it explicitly asks for one of the candidate models, and — when the session has earlier turns and a recorded previous model — whether it continues the subject of the last user message. The work answer picks the tier order and the reasoning level:

| Work | Tier order | Reasoning |
|---|---|---|
| `code` | S > A > B > C | `xhigh` |
| `research` | S > A > B > C | `high` |
| `work` (the default for anything else) | A > S > B > C | `medium` |
| `chat` | B > C > A > S | `none` |
| `fetch` | C > B > A > S | `low` |

A model named in the request goes first; when none is named and the subject is unchanged, the session's previous model takes that slot so its prompt cache is reused. When the previous model leads this way, the reasoning level stays at the previous one whenever it is a valid level, and these leading picks are not reshuffled by provider priority; only the ranked candidates after them are. The previous model is recorded after each successful reply and expires after 30 minutes for `openai` / `codex`, 60 minutes for `gemini`, and 5 minutes for other providers. The remaining candidates are sorted by tier, reading `model_tag` first and falling back to the naming rules; within a tier, `claude-code` and then `codex` models come first, then ties break by model family. For `research` work, `copilot@` models move behind the others of the same rank because of Copilot's smaller context window. `pass` models are left out of this ranking. The request context is the last 4 user and assistant turns, each cut at 2,048 runes.

The TypeSafe/Jev beta dispatcher (`dispatcher_beta` calling `jev-latest` directly, with its own `TYPESAFE_API_KEY` prompt and the `missing_key` 400) was Removed in v1.3.1; the decision model above replaces it, and TypeSafe is now one of its providers.
