Remembers the effort level for each model and restores it when you switch back

A Claude Code mod that gives every model its own effort level.
Claude Code keeps one effort setting for the whole session. Switch from Sonnet on high to Opus, set medium to plan, switch back to Sonnet to implement, and Sonnet is now on medium too. With this mod, Sonnet comes back on high.
In a terminal session of Claude Code (v2.1.287 or later):
/plugin install effort-per-model --marketplace malitov/effort-per-model
Answer y to add the marketplace, then pick a scope (user scope makes it load in every session, the Desktop app's Code tab included).
/effort <level> with that model's saved effort, so the session and the UI switch right away. A toast says effort high restored for claude-sonnet-5-5.Works with /model, the model picker, opusplan and plan mode, the Desktop app's model menu: the mod watches what each request is actually sent with, not how it was chosen.
/model picker) is overridden by the saved one; set it with /effort after switching.--effort.claude plugin validate .
claude plugin test .
claude --plugin-dir .
hooks/decide.ts holds the decision, hooks/register.ts wires it to the turn.step event.
MIT
hooks/register.ts 53 lines1import type { Register } from 'claude-code'
2import { decide, type Effort, type Seen } from './decide'
3
4const KEY = 'byModel'
5let seen: Seen = {}
6let polled: string | undefined
7let applied: string | undefined // `${model}:${effort}` last put on the session
8
9async function saved($: any): Promise<Record<string, Effort>> {
10 return ((await $.store.get(KEY)) ?? {}) as Record<string, Effort>
11}
12
13function restore($: any, model: string, effort: Effort) {
14 if (applied === `${model}:${effort}`) return
15 applied = `${model}:${effort}`
16 // queued until the session is idle; makes it the session's effort, so the UI shows it
17 $.command.run({ command: 'effort', args: String(effort) }).catch(() => {})
18 $.ui.toast(`effort ${effort} restored for ${model}`)
19}
20
21export const register: Register = on => {
22 // Switch the effort the moment the model changes, before any prompt is sent.
23 on('session.start', async ($, e, next) => {
24 const r = await next(e)
25 // ponytail: 1s poll, the mod API has no model-changed event
26 $.clock.every(1000, async () => {
27 const model = await $.session.model()
28 if (model === polled) return
29 polled = model
30 const effort = (await saved($))[model]
31 if (effort !== undefined) restore($, model, effort)
32 })
33 return r
34 })
35
36 // Every request: record the effort per model, and restore it if the poll hasn't yet.
37 on('turn.step', async function* ($, e, next) {
38 // subagents run with their own effort; leave them be
39 if (e.agentId !== undefined || e.effort === undefined) return yield* next(e)
40
41 const byModel = await saved($)
42 const d = decide(seen, e.model, e.effort, byModel[e.model])
43 seen = d.seen
44 polled = e.model
45
46 if (d.restore) restore($, e.model, d.use!)
47 applied = `${e.model}:${d.use}`
48 if (byModel[e.model] !== d.use) await $.store.set(KEY, { ...byModel, [e.model]: d.use })
49
50 return yield* next(d.restore ? { ...e, effort: d.use } : e)
51 })
52}
53hooks/decide.ts 13 lines1export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | number
2export type Seen = { model?: string; effort?: Effort }
3
4// Model changed and the session's effort is still the one carried over
5// (not touched since the last request) → use what this model had last time.
6export function decide(seen: Seen, model: string, effort: Effort | undefined, saved: Effort | undefined) {
7 const carried = seen.model === undefined || effort === seen.effort
8 const restore = effort !== undefined && model !== seen.model && carried && saved !== undefined && saved !== effort
9 const use = restore ? saved : effort
10 // a model without effort leaves the last seen effort as it was
11 return { use, restore, seen: { model, effort: use ?? seen.effort } }
12}
13