How hard the model thinks

A reasoning model spends time thinking before it answers, and how much is a per-request setting: reasoning_effort on the OpenAI wire, which OpenRouter forwards to the model. It is the single biggest lever on how long a round takes.

The rungs

Resolved by ReasoningEffortResolution where AgentChatClient builds the round's one ChatOptions, so a single decision covers every provider call the round makes, tool-loop iterations included. First non-blank wins.

# Where Who sets it
1 ThreadComposer.Effort — the /effort pick the person chatting
2 AgentConfiguration.ReasoningEffort — reasoningEffort: in the agent's front matter the agent's author: this is where the WORK is known
3 ModelDefinition.ReasoningEffort on the LanguageModel node an admin, for the whole deployment
4 ModelTierDefinition.ReasoningEffort — the agent's tier, else chat the tier registry (Provider/Tier/*)
— nothing is sent the provider decides

The provider's default is not neutral, and it is what the Executive Assistant ran at. Measured on OpenRouter's model API (reasoning block of GET /api/v1/models, 2026-09-30): z-ai/glm-5.3 is mandatory: true, supported_efforts: [max, high, low], default_effort: max. Before rungs 2 and 4 existed, a thread started without effort on an agent that pins nothing (the Executive Assistant) sent no reasoning_effort and no max_tokens at all, so GLM-5.3 thought at max — an earlier round of nine cheap tool calls took 3 min 11 s that way (AI/RoundTiming).

Rung 4 is the declared default. The shipped tiers carry: utility medium · chat medium · reasoning high · review max · coding xhigh. An agent that names no tier is an interactive one and resolves against chat, so an agent authored without an effort still runs at medium. A tier node with a blank value inherits the shipped value for its id — tier nodes are seeded create-if-absent, so every node that predates the field is blank, and reading blank as "send nothing" would have made the default a no-op on every existing deployment. An operator changes the default by editing the tier node (reasoningEffort), which wins over the shipped value.

A blank rung 1 is declining, not choosing. Picking Model default writes an empty string, which hands the decision back to rungs 2–4.

Every shipped agent declares its effort

No blanks: AgentReasoningEffortDeclaredTest fails the build on an agent under any */Agent/ folder without a known reasoningEffort. Chat and assistant agents run at medium (a person is waiting), utility micro-jobs at medium (moderate, not none — they still decide something), coding agents at xhigh (a wrong patch costs more than a slow one). The rest are justified row by row.

Agent Tier Effort Why
Assistant (default) — medium interactive chat
Executive Assistant — medium interactive assistant (was: provider default = GLM max)
Email Router — medium assistant work on an inbound mail
Researcher — medium a chat's delegate — the person is still waiting
Worker — medium a chat's delegate for bulk writes, not code
Tutor — medium interactive teaching
TrainingSim — xhigh writes the C# cell a learner runs — writing code
Description Writer · Node Initializer · Pull Request Writer · Thread Namer utility medium utility
Notification Triage light (= chat) medium utility decision per event
Log Triage reasoning high analysis that ends in a ticket, not code
Feedback Agent — medium (from the chat tier) interactive capture. Its node is JSON, and GitSync reads JSON strictly, so an image that predates the field would refuse the whole snapshot. The field goes into the file once this release is on every portal; until then rung 4 gives it medium. The guard lists it by name and fails once the file declares one
RemoteControl · Voice · Music — / chat medium interactive; latency is the product
Quality Time · Quality Time Deutsch reasoning medium spoken interactive host — the tier would say high
bug-triage coding xhigh writes the fix and its test
devops — xhigh coding-tier work: proposes code and PR changes
reviewer (Governance) — high validates a proposal against evidence; analysis, not code
live-defect-triage · loki-error-triage · platform-update · pr-babysitter · triage reasoning high diagnosis and classification, not code
pr-reviewer review max decision D1 of Governance/PullRequestSteward — one careful pass per head; the round-length fix is the tool-call budget (Plugins #2577), not a lower effort

Satellite repositories declare theirs the same way (see their agent folders).

Sent in the model's own vocabulary

Providers do not share one vocabulary, and OpenRouter maps an unsupported level to "the nearest supported" one — which for a level BETWEEN two supported ones is an undocumented tie. GLM-5.3 has no medium (between its low and high) and no xhigh (between its high and max), so the chat default and the coding default would each have been a coin the gateway flips. The OpenAI-wire client (OpenAIReasoningEffortWire) decides instead: the level is snapped to the weakest accepted level at or above it (ReasoningEffortLevels.Snap) and sent under that exact spelling, so a request is never silently weakened.

Resolved GLM-5.3 receives gpt-5.2 Claude 5 line
medium high medium medium
high high high high
xhigh max xhigh xhigh
max max xhigh xhigh ¹

The accepted levels come from ModelDefinition.SupportedReasoningEfforts on the node, else the built-in ModelReasoningEfforts table (read from OpenRouter's model API, like ModelPricing), else "unknown" — and then the level is sent unchanged. ¹ The abstraction's top member is ExtraHigh, so a resolved max leaves the engine as xhigh: on a model that accepts only max it is sent as max, on one that accepts both it is sent as xhigh.

One accepted warning suppression. Sending a spelling the abstraction cannot express (max) means setting ChatCompletionOptions.ReasoningEffortLevel, which the OpenAI SDK marks for evaluation (OPENAI001). OpenAIReasoningEffortWire disables that one diagnostic around that one assignment, with a disable/restore pair and the reason inline. That is the same scoped form the repository uses for CS0618 and CA1416. This is a deliberate, recorded exception, not a precedent for silencing warnings. If the SDK promotes the API to stable, delete the pair.

Choosing it

An unknown spelling costs the SETTING, never the model: the round logs one warning naming the spelling and the rung it came from, and sends nothing.

A known distortion that no effort setting fixes

An OpenRouter key with PII masking enabled redacts names, e-mail addresses and code in every request behind it — the model sees [PERSON_NAME] and [EMAIL] instead of the text. That is an ACCOUNT setting on the key, not a per-request one, so it distorts every model on that key equally and no effort level changes it. Read a round that "cannot see" a name or an address against the key's settings before tuning the effort.

Under the Claude Code harness

/effort belongs to the CLI there, and the portal forwards the command 1:1 — the levels are the CLI's own and the chip shows what the CLI reports. The mesh rungs above apply to the MeshWeaver harness, which is the one that talks to a provider itself.

Why the picker takes VALUES, not nodes

/effort is the first Pick skill whose options are not mesh nodes. SkillAction.Choices is that shape: a fixed list of {value, label, description} on the skill's own front matter, rendered by the same widget, with the same keyboard handling and the same row anatomy as /agent and /model, and written to the named composer field verbatim. The alternative — seeding five ReasoningEffort nodes into every mesh before the setting can be chosen at all — makes a word the provider already understands into a deployment step.

Any skill can use it: declare choices: instead of query: under action:. A skill that declares both keeps its query, so nothing existing changes meaning.

What the menu shows

One row shape for every pick (ThreadChatView.PickerRow): icon column, name, one meta line, and a ✓ on whatever is selected right now. A model's meta line is derived from its definition — how hard it thinks, and its per-million prices — because a model node carries no description and a row that only repeats the wire id tells the person choosing nothing they did not already type.

Models group under their provider's title by the nearest title that OWNS their namespace, not by the namespace itself. Every model the catalog sync writes sits directly under its provider with the vendor prefix inside the id (Provider/OpenRouter + openai/gpt-5.2); a model created by hand splits that id at the slash and lands one level deeper (Provider/OpenRouter/z-ai + glm-5.3). Both are the same model under the same provider, and before this the second one rendered as an untitled row after the whole block — configured, pinned to the top of its provider by Order: -2, and unfindable by anyone scrolling for it.