Codumentor logo Codumentor

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:

ModeWhat it does
FastQuick answers, no deliberation. Good for chat and lookups.
BalancedReasons when it helps. The everyday default.
DeepThinks 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:

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:

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

SurfaceHow
WebThe 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 APISend 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.

ParameterTypeDefaultDescription
enabledbooltrueWhether the plugin is active.
data_dirstringagent storage dir, else ./dataWhere the per-conversation selections are stored.
max_conversations_per_userint2000How 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.

FieldTypeDefaultDescription
titlestringrequiredLabel shown in the picker.
descriptionstring""One line under the title.
iconstringunsetIcon token for the picker.
costint 1–32Relative cost/speed badge.
mainprofile nameunsetModel the main agent uses, and what unlisted roles inherit. Unset leaves model selection to models:.
rolesmap{}role → profile name, overriding main for that role.
effort.defaultrungunsetRung the mode starts at. Unset means "whatever the model's own default is".
effort.allowedlist of rungsunsetRungs the user may select. Unset means every rung the model supports. Always intersected with the model's capability.
promptstringunsetInstructions 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