Thinking Modes Plugin
The Thinking Modes plugin gives every conversation a thinking mode — one
named setting that decides which model answers, which model each background
role uses, and how hard the assistant reasons — plus a reasoning effort
rung the user can move within that mode.
Mode and effort belong to the conversation: switching one conversation never
changes another, and each user can set which mode their new conversations
start in.
Overview
Three modes ship out of the box and need no configuration:
| Mode | What it does |
|---|---|
| Fast | Quick answers, no deliberation. Good for chat and lookups. |
| Balanced | Reasons when it helps. The everyday default. |
| Deep | Thinks hard before answering. For design and tricky bugs. |
They differ in reasoning effort only and name no model, so enabling the plugin
on an existing deployment changes nothing until someone picks a mode: Balanced
asks the model for no particular effort, which is exactly what happened before.
A deployer can replace them with their own curated set (below) — for example a
Deep mode that also switches the main agent to a stronger endpoint while
leaving the summariser on a cheap one.
Reasoning effort is an ordinal scale — off · low · medium · high · max —
and an intent, not a token budget. What a rung means on the wire is decided
per model by the reasoning: record on its profile (see
Model Profiles and the models: section of the admin
configuration). A rung a model cannot honour is never sent: it is clamped to
the nearest one the model supports, so the endpoint never sees a value it
would reject.
Two rules are worth knowing because they are easy to be surprised by:
- Roles a mode does not list inherit its main model, and a mode that names no main model leaves model selection exactly where the
models:block already put it. - Background roles reason at the mode's default effort, not at whatever the user is currently set to. Turning a conversation up to max makes the assistant think harder; it does not make the summariser think harder too.
Choosing a mode
In Settings → Models
Settings → Models → Thinking modes lists every mode as a card: its title,
its one-line description, the model the main agent would run on, what effort it
starts at, and a relative cost badge. Set default makes a mode the one your
new conversations start in; Clear default hands that decision back to the
deployer's configured default. Nothing here changes a conversation you already
have — that is the composer's job, and one conversation never moves another.
The cards are read-only. Modes are declared by the deployer (below); there is no
mode editor.
Settings → Models → Role map shows, for whichever mode you select, every
role that asks for a model: its name in words, one line saying what it does, the
connection it resolves to and the model id that connection is — plus why
(assigned, inherited, server, built-in model). It is the answer to "so
which model actually summarises my conversation?" without reading any config.
Custom is always in both lists, even if you have never set a per-role
assignment, so choosing it — or getting back to it after trying a deployer's
mode — is a click rather than a slash command. Selecting Custom in the Role map
turns the connection column into dropdowns: those are your own
Model Profiles assignments, prefilled.
Two API routes back that page, neither of them conversation-scoped:
GET /ui/plugins/thinking_modes/settings
→ { modes, default_mode, user_default, builtin, roles, role_maps }
PUT /ui/plugins/thinking_modes/user-default
{ "mode": "deep", "effort": "high" } → the same payload, updated
The PUT replaces the default, so an empty body ({}) clears it. role_maps
is keyed by mode id, and each row is
{role, profile, model, source, editable} — editable is true only for
custom. An unknown mode or a rung off the scale is a 400; a rung the model
cannot honour is not (it clamps, as everywhere else).
In a conversation
Mode and effort belong to the conversation, so they are changed with a patch on
it:
PATCH /ui/plugins/thinking_modes/conversations/{conversation_id}/thinking
{ "mode": "deep", "effort": "high", "set_as_user_default": false }
Every field is optional and an omitted one is left alone. effort: null is
different from omitting it: null means "ask the model for no particular
effort and let its own default stand". set_as_user_default: true also makes
the pair the starting point for your new conversations.
The response is the same object the conversation snapshot carries under
thinking, plus what changed:
{
"selection": { "mode": "deep", "effort": "high", "source": "conversation" },
"resolved": { "title": "Deep", "effort": "high", "requested_effort": "high",
"main_model": "claude-sonnet-4-6", "effort_available": true,
"rungs": [ { "rung": "off", "enabled": false,
"reason": "The Deep mode does not offer off effort." } ] },
"modes": [ … ],
"user_default": null,
"change": { "changed": true, "mode_changed": true },
"seam": { "text": "Switched to Deep · High (claude-sonnet-4-6)" }
}
GET on the same path returns everything except change and seam.
Two answers are worth reading rather than assuming:
resolved.effortis what will actually be sent, andrequested_effortis what was asked for. They differ when the model cannot honour the rung — asking for one it does not support is never an error, it is clamped.resolved.effort_available: falsemeans the effort control is dead for this conversation's model: either it has no reasoning control, or the endpoint refused one and the plugin stopped sending them.effort_unavailable_reasonsays which.- Each entry in
modescarries bothmainandmain_model.mainis the profile the mode declares (nullwhen it declares none and simply inherits);main_modelis the model id that resolves to for this user, which is what answers "would picking this switch the brain?".main_modelisnullonly forcustom, which declares nothing — see below.
When mode or effort actually changes, a marker is added to the transcript —
"Switched to Deep · High (claude-sonnet-4-6)" — so the conversation records
which brain answered which question. Re-picking the mode you are already in
changes nothing and adds no marker.
Outside the web UI
| Surface | How |
|---|---|
| Web | The chip at the left of the composer switches this conversation; Settings → Models (above) decides where new ones start. |
| Terminal UI | /mode <name>, /effort <rung> and /thinking, typed in the composer — the same words as Telegram. The pair in force is always on screen in the composer's status strip (◐ Balanced · High, shortened to ◐ High on a narrow terminal), and a change leaves the same marker in the transcript that the web shows. With the plugin absent there is no strip and /mode is an ordinary message. |
| Telegram | /mode <name>, /effort <rung>, and /thinking for the current pair. /mode or /effort on their own list what is available. Anything else beginning with / is passed to the assistant untouched. |
codumentor -p | --mode <name> and --effort <rung>, e.g. codumentor -p --mode deep "why is this test flaky?". The pair is echoed on stderr; with --output-format stream-json it also appears in the init event as thinkingMode / reasoningEffort. Both flags apply to print mode only. |
| OpenAI-compatible API | Send the mode name as the request's model field — {"model": "deep", …}. Any other value keeps its existing meaning (the field is otherwise ignored). GET /v1/models lists the modes alongside codumentor-agent. |
A mode name that does not exist, or a rung that is not on the scale, is
rejected and told to you; a rung that exists but the model cannot honour is
accepted and clamped.
Users who already assign their own models
If a user has set per-role assignments through
Model Profiles, those assignments keep working and appear
as an implicit Custom mode. Nothing changes for them until they pick one of
the deployer's modes; picking one overlays its role map for that conversation,
and their own named profiles stay available.
Custom is never lost: it is listed in Settings → Models whether or not any
assignment exists, so it can be made the default for new conversations, and
/mode custom puts the current conversation back in it.
Configuration
plugins:
- module: codumentor.plugins.thinking_modes
class: ThinkingModesPlugin
priority: 6
args:
enabled: true
Enabled in the default shipped config. Register it at a **higher priority
number than model_profiles** (which ships at 5) so a mode layers on top of a
user's own assignments rather than the other way round.
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether the plugin is active. |
data_dir | string | agent storage dir, else ./data | Where the per-conversation selections are stored. |
max_conversations_per_user | int | 2000 | How many conversations' selections are kept per user before the oldest are dropped. |
Declaring your own modes
Modes live in a top-level thinking_modes: section of the main configuration,
next to models: — not in the plugin's args:, because they reference the
profiles declared there and are cross-checked against them when the config
loads.
thinking_modes:
default: balanced # mode for a user who has expressed no preference
modes:
fast:
title: Fast
description: Quick answers, no deliberation. Good for chat and lookups.
icon: bolt
cost: 1 # 1-3 relative badge shown in the picker
main: local-qwen # a name from models.profiles
effort: { default: "off", allowed: ["off", "low"] }
balanced:
title: Balanced
description: Reasons when it helps. The everyday default.
main: local-qwen
effort: { default: medium, allowed: ["off", "low", "medium", "high"] }
deep:
title: Deep
description: Plans before acting, strongest model for code changes.
icon: brain
cost: 3
main: cloud-strong
roles:
context_summarization: local-qwen
explore: local-qwen
effort: { default: high, allowed: [medium, high, max] }
prompt: |
Before editing, write a short plan and verify it against the code.
Declaring any mode replaces the built-in three entirely.
| Field | Type | Default | Description |
|---|---|---|---|
title | string | required | Label shown in the picker. |
description | string | "" | One line under the title. |
icon | string | unset | Icon token for the picker. |
cost | int 1–3 | 2 | Relative cost/speed badge. |
main | profile name | unset | Model the main agent uses, and what unlisted roles inherit. Unset leaves model selection to models:. |
roles | map | {} | role → profile name, overriding main for that role. |
effort.default | rung | unset | Rung the mode starts at. Unset means "whatever the model's own default is". |
effort.allowed | list of rungs | unset | Rungs the user may select. Unset means every rung the model supports. Always intersected with the model's capability. |
prompt | string | unset | Instructions injected as a stable prefix note whenever this mode is active. |
Roles you may name in roles: are subagent, agentic_developer,
agentic_memory_store, context_summarization, explore, goal,
web_retrieve, web_search, auto_title, translation, voice_mediator,
plus any role you already point at a profile in models.subagent_profiles.
Write "off" in quotes: unquoted off is a YAML boolean. (Both are accepted —
this is a readability note, not a requirement.)
Notes
- A bad mode fails at startup, not at the first turn. A
main:orroles:entry naming a profile that does not exist, a role nobody answers to, a default effort outside the allowed list, or adefault:naming no mode — all of them stop the configuration from loading, and the error names every problem it found at once. - Switching modes mid-conversation is allowed and cheap to reason about, but not free. A different model — or a different effort on the same model — means a cold prompt-prefix cache for that conversation's next request.
- A mode never widens what the assistant may do. It selects models, reasoning effort and an optional instruction block; tool access stays whatever the access profile allows.
- What is stored: a mode name and a rung per conversation, plus a per-user default. Nothing sensitive, so unlike Model Profiles' API keys there is nothing here to encrypt.
- Plugin authors can read the active mode for a turn from
ctx["thinking_mode"]andctx["reasoning_effort"], both set beforeonPromptAssemble, and can subscribe to theplugin.thinking_modes.mode_changedconversation event to see a switch as it happens.