Routes each main-thread turn to a heavy or light model based on the prompt, and falls back to the light model near your rate limits.

Four Claude Code mods (function-hook plugins) that save tokens and keep you aware of your limits.
| Mod | What it does |
|---|---|
smart-model-router | Picks a heavy or light model for each turn |
usage-bar | Shows 5-hour and weekly rate-limit usage above the prompt |
context-bar | Shows how full the context window is, by category, above the prompt |
research-offloader | Keeps research work out of the main context |
Kaizen is continuous improvement through small changes, each removing one source of waste (muda). These mods apply it to a Claude Code workflow:
| Waste | Mod that removes it |
|---|---|
| The most expensive model doing routine work | smart-model-router |
| Raw web pages filling the main context | research-offloader |
| Hitting a rate limit by surprise | usage-bar (and the router's usage guard) |
| Context filling up unnoticed | context-bar |
None of these is a big idea; each is one small fix. Run the cycle on your own usage:
usage-bar.usageGuard, the model options and the research trigger phrases, then repeat.What you can see, you can improve.
claude --plugin-dir D:\ai\cc-mods\smart-model-router --plugin-dir D:\ai\cc-mods\usage-bar --plugin-dir D:\ai\cc-mods\context-bar --plugin-dir D:\ai\cc-mods\research-offloader
Saving a file reloads the mod in a running session.
/plugin install smart-model-router --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install usage-bar --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install context-bar --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install research-offloader --marketplace timothylok/ClaudeCodeModsByTimLok
Answer y to add the marketplace, then choose a scope. Each mod is active immediately.
Mods with options show them as rows in /config, or you can set them in settings.json under pluginConfigs.<mod>.options. Changing one reloads the mod.
Chooses a model for every turn and sends the main thread's requests to it. Subagents keep their own model.
How a model is chosen, in order:
heavy or light, use that model.auto mode, a prompt that mentions architecture, designing a system, multi-file work, rewriting the whole thing, deep reasoning, trade-offs, migrations, root cause, or planning, or one longer than 2,500 characters, gets the heavy model. Anything else gets the light model.The status line shows the choice and the reason, e.g. router: claude-opus-5-5 (complex prompt).
| Command | Effect |
|---|---|
/route | Show the current mode |
/route auto | Pick per prompt (default) |
/route heavy | Always use the heavy model |
/route light | Always use the light model |
/route off | Router does nothing; the session's own model is used |
The mode is remembered across sessions.
| Option | Default | Meaning |
|---|---|---|
heavyModel | claude-opus-5-5 | Model for complex turns |
lightModel | claude-sonnet-5-5 | Model for everyday turns |
usageGuard | 85 | Usage % above which the light model is always used |
| Prompt | Model |
|---|---|
Design the architecture for a multi-tenant billing service | heavy |
Plan out a migration from REST to gRPC, with the trade-offs | heavy |
Find the root cause of this flaky test | heavy |
fix the typo in README | light |
write a unit test for parseDate | light |
Switching models starts a new prompt cache, so flipping often costs extra input tokens. Use /route heavy or /route light to pin a model for a stretch of work.
A band above the prompt with a meter for each rate-limit window Claude Code reports:
Usage 5h █████░░░░░ 42% 2h30m week █████████░ 91% 3d
| Command | Effect |
|---|---|
/usage-bar | Hide or show the band (toggle) |
None.
A stacked bar above the prompt showing the context window, one colour per /context category, with a legend of the biggest categories:
████████▒▒▒▒░░░░░░░░░░░░░░░░░░░░ 38% · 76k/200k
■ Messages 41k ■ System tools 18k ■ Memory files 6k
█ is used space, ▒ is the autocompact buffer, ░ is free space.usage-bar and stacks with it. It is hidden while a survey is showing.| Command | Effect |
|---|---|
/context-bar | Hide or show the bar (toggle) |
None.
When you type a research-style prompt, the mod keeps the page fetching and reading out of the main thread, so only a summary enters your context.
What counts as research: prompts containing "research", "look up", "summarize this url/page/article/docs", "compare sources", "read this page/article/docs", "what is the latest", or "search the web". It only reacts to prompts you type yourself.
Two modes, depending on the endpoint option:
{ "query": "<your prompt>" } to it, expects { "summary": "..." } back, and gives Claude that summary as context. If the endpoint fails or returns no summary, it falls back to the subagent mode below.subagentModel.A toast tells you which mode was used.
| Option | Default | Meaning |
|---|---|---|
endpoint | empty | URL of your own research bridge (for example a NotebookLM wrapper). Leave empty to use subagents |
subagentModel | haiku | Model for research subagents: haiku, sonnet or opus |
| Prompt | Result |
|---|---|
Research the best Rust ORMs in 2026 | offloaded |
Can you look up how Vite handles HMR? | offloaded |
summarize this page for me: https://example.com | offloaded |
compare sources on WebGPU support | offloaded |
fix the failing test in auth.ts | untouched |
NotebookLM has no public API, so the endpoint is yours to provide. Any service that takes {query} and returns {summary} works.
usage-bar hid other bands above the promptusage-bar installed, a separate context bar disappeared.ui.render on AbovePrompt). The outermost hook returned its own tree without calling next(e), so the hook beneath it never ran.usage-bar now calls next(e) and stacks what comes back under its own row. context-bar does the same from 0.1.4, so the two show together in either order.ui.render hook on a shared site must await next(e) and include the result in its tree. In tests, add a stand-in ui.render hook beneath the mod so next has something to return.Each mod has hooks/register.ts(x), a manifest in .claude-plugin/plugin.json, and tests in tests/.
claude plugin validate <mod folder>
claude plugin test <mod folder>
hooks/register.ts 93 lines1import type { Register } from 'claude-code'
2
3type Mode = 'auto' | 'heavy' | 'light' | 'off'
4
5const HEAVY = [
6 /\barchitect(ure|ing)?\b/,
7 /\bdesign (a|the) (system|service|api|schema)\b/,
8 /\bmulti[- ]file\b/,
9 /\brefactor (everything|the (whole|entire))\b/,
10 /\b(re)?write the (whole|entire)\b/,
11 /\bdeep(ly)? (reason|think|dive)\b/,
12 /\btrade-?offs?\b/,
13 /\bmigrat(e|ion) (from|to|the)\b/,
14 /\broot cause\b/,
15 /\b(plan|blueprint) (for|out)\b/,
16]
17
18export const isHeavyPrompt = (text: string): boolean => {
19 const lower = text.toLowerCase()
20
21 return HEAVY.some(re => re.test(lower)) || lower.length > 2500
22}
23
24export const register: Register = (on, options) => {
25 const heavyModel = String(options.heavyModel ?? 'claude-opus-5-5')
26 const lightModel = String(options.lightModel ?? 'claude-sonnet-5-5')
27 const usageGuard = Number(options.usageGuard ?? 85)
28
29 let mode: Mode = 'auto'
30 // The model this turn's main-thread steps go to; null leaves the session's own.
31 let turnModel: string | null = null
32
33 on('session.start', async ($, e, next) => {
34 const stored = await $.store.get('mode')
35 if (stored === 'auto' || stored === 'heavy' || stored === 'light' || stored === 'off') mode = stored
36 await $.command.register({
37 name: 'route',
38 description: 'Model router: /route auto | heavy | light | off',
39 })
40
41 return next(e)
42 })
43
44 on('command.run', { command: 'route' }, async ($, e) => {
45 const want = e.args.trim().toLowerCase()
46 if (want !== 'auto' && want !== 'heavy' && want !== 'light' && want !== 'off') {
47 return { text: `Router mode is "${mode}". Use /route auto | heavy | light | off.` }
48 }
49 mode = want
50 await $.store.set('mode', mode)
51 if (mode === 'off') $.ui.status(undefined)
52
53 return { text: `Router mode set to "${mode}".` }
54 })
55
56 on('prompt.submit', async ($, e, next) => {
57 if (mode === 'off') {
58 turnModel = null
59
60 return next(e)
61 }
62
63 const { rateLimits } = await $.session.usage()
64 const peak = Math.max(0, ...rateLimits.map(r => r.percentUsed))
65 let why: string
66
67 if (peak >= usageGuard) {
68 turnModel = lightModel
69 why = `usage ${peak}%`
70 } else if (mode === 'heavy' || mode === 'light') {
71 turnModel = mode === 'heavy' ? heavyModel : lightModel
72 why = 'forced'
73 } else {
74 const heavy = isHeavyPrompt(e.text)
75 turnModel = heavy ? heavyModel : lightModel
76 why = heavy ? 'complex prompt' : 'routine prompt'
77 }
78 $.ui.status(`router: ${turnModel} (${why})`)
79
80 return next(e)
81 })
82
83 // Every model request of the main thread in this turn goes to the chosen model;
84 // subagents keep whatever model they were spawned with.
85 on('turn.step', async function* ($, e, next) {
86 if (turnModel === null || e.agentId !== undefined || e.model === turnModel) {
87 return yield* next(e)
88 }
89
90 return yield* next({ ...e, model: turnModel })
91 })
92}
93