SLOPSHOPPER

model-routing

Routes every delegated agent to the cheapest model tier that can do the job well, at an explicit effort, with spawn limits and a routing guide Claude always…

newguardpromptagents
★ 1v0.1.0no licenseupdated 2026-10-14ataege/claude-skills/plugins/model-routing
A shopper browsing a rack in a slop shop
README

claude-skills

A Claude Code plugin marketplace.

PluginWhat it does
clean-viewHides tool calls, diffs and command output, and shows a calm checklist of Claude's plan above the prompt, with a row for each helper Claude starts. /simple on or /simple off switches it. Also holds Agent Dock: pick a Team Size from 1 to 100 and every request is split across that many helpers, each shown live in a pane. /dock opens or folds it, /dock 10 sets the size.
status-barOne colored line under the prompt: plan, model, effort, context, 7-day and 5-hour limits, and running agents by model and effort.
model-routingRoutes every delegated agent to the cheapest model tier that can do the job well, at an explicit effort. Refuses agents with no model, fills in effort by tier, blocks spawns from haiku agents and past two levels, and keeps a routing guide in Claude's instructions.
mods-menuA ☰ Mods button beside the Clean View toggle. It opens a small menu listing your mods with their controls: Clean View on or off, the Agent Dock and its Team Size, and the mods that are always on. /mods opens or closes it.
prompt-upgradeTurns a rough prompt into a great one before it is sent. /better <prompt> opens a pane that asks a few quick multiple-choice questions, rewrites the prompt, and shows your original and the upgrade side by side; nothing runs until you press Run. /better on checks every typed prompt first: one that already has what it needs is sent as typed, one that falls short opens the pane, and the same prompt sent twice goes straight through. Press Enter twice to send a prompt as typed. A Detail setting (Auto, Low, Medium or High; /better level auto) sets the questions and how long and deep the upgrade is: Low, Medium and High ask up to 3, 5 or 7; Auto also turns automatic mode on, asks as many questions as needed (up to 9) and, if you don't answer within 30 seconds or press Do it for me, fills in the rest and sends the upgrade itself. /better history shows the last 20.

Install

At the prompt of a terminal session:

/plugin install clean-view --marketplace ataege/claude-skills
/plugin install status-bar --marketplace ataege/claude-skills
/plugin install model-routing --marketplace ataege/claude-skills
/plugin install mods-menu --marketplace ataege/claude-skills
/plugin install prompt-upgrade --marketplace ataege/claude-skills

Answer y to add the marketplace, then pick a scope with Enter.

From a shell:

claude plugin install clean-view --marketplace ataege/claude-skills
claude plugin install status-bar --marketplace ataege/claude-skills
claude plugin install model-routing --marketplace ataege/claude-skills
claude plugin install mods-menu --marketplace ataege/claude-skills
claude plugin install prompt-upgrade --marketplace ataege/claude-skills

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with ataege/claude-skills, then Add plugin.

Developing

Each plugin lives in plugins/<name>. Check one with:

claude plugin validate plugins/<name>
claude plugin test plugins/<name>
Source 3 files
hooks/register.ts 9 lines
1import type { Register } from 'claude-code'
2
3import { registerModelRouting } from './model-routing'
4
5// Each mod registers itself here, so more can sit beside model routing later.
6export const register: Register = on => {
7  registerModelRouting(on)
8}
9
hooks/model-routing.ts 221 lines
1// Model routing: each delegated agent runs on the cheapest tier whose judgment is enough, at an explicit effort.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, On } from 'claude-code'
4
5import type { ModelRoutingAgent, ModelTier } from '../types'
6
7// ---------------------------------------------------------------------------
8// Tiers, defaults and limits
9// ---------------------------------------------------------------------------
10
11type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
12
13const TIER_RANK: Readonly<Record<ModelTier, number>> = { haiku: 1, sonnet: 2, opus: 3, fable: 4 }
14
15// The effort a tier runs at when Claude names the tier but not the effort (the skill's routing table).
16export const DEFAULT_EFFORT: Readonly<Record<ModelTier, Effort>> = {
17  haiku: 'low',
18  sonnet: 'medium',
19  opus: 'high',
20  fable: 'xhigh',
21}
22
23// Agent types with no model of their own: without `model` they run on the session's model.
24const UNPINNED_AGENT_TYPES: ReadonlySet<string> = new Set(['general-purpose', 'claude'])
25
26// Agents nest at most this deep: the main conversation's agents are depth 1.
27const MAX_SPAWN_DEPTH = 2
28// The agent table keeps the most recent spawns only.
29const MAX_TRACKED_AGENTS = 300
30
31const agentsAtom = atom({ plugin: 'model-routing', key: 'agents' } as const, {})
32const typeTiersAtom = atom({ plugin: 'model-routing', key: 'typeTiers' } as const, {})
33
34type Engine = EngineInterface
35
36/** The tier a model alias or id names: "claude-haiku-5-5" and "haiku" are both haiku. */
37export function tierOf(model: unknown): ModelTier | null {
38  if (typeof model !== 'string') return null
39  const family = model.toLowerCase().match(/haiku|sonnet|opus|fable/)?.[0]
40
41  return (family as ModelTier | undefined) ?? null
42}
43
44function isAboveCeiling(requested: ModelTier, sessionTier: ModelTier | null): boolean {
45  return sessionTier !== null && TIER_RANK[requested] > TIER_RANK[sessionTier]
46}
47
48// ---------------------------------------------------------------------------
49// What Claude reads: the always-on guide and the fixes for refused calls
50// ---------------------------------------------------------------------------
51
52/** The routing guide added to Claude's instructions whenever agent tools are available. */
53export function routingGuide(sessionTier: ModelTier | null): string {
54  const session = sessionTier ?? 'the session model'
55  return `# Model routing
56Delegated work runs on the cheapest tier whose judgment is enough. This session runs on ${session}, which is the ceiling for delegated work, not the default. Effectiveness comes first: never pick a tier too weak for the task to save money.
57
58On every Agent call, set \`model\` and \`effort\` yourself as this table says. This is the explicit instruction the Agent tool's effort field asks for. Before a task's first spawn, state the routing in one line, like \`Routing: 6 × haiku/low (rename batches), 1 × opus/high (JWT design).\`
59
60| Subtask | model | effort |
61|---|---|---|
62| Deterministic: count, sort, grep, a sed-able rename | no agent, use the shell | – |
63| Output fixed by the input plus a template, a wrong result is cheap to spot: renames, short summaries, extracting fields, formatting, run a command and report | haiku | low |
64| Must decide what matters: scoped research, locating code, explaining a subsystem, drafting, a contained change with a clear spec | sonnet | medium |
65| Judging or verifying findings, design tradeoffs, migrations, auth, payments, data changes | opus | high, or xhigh when regressions are costly |
66| Final synthesis that must hold everything in view | ${session} | leave effort out |
67| Core of a long-horizon problem, only when the session is on fable | fable | xhigh or max |
68
69Prices in $ per million tokens in / out: haiku 0.10 / 0.50, sonnet 2 / 10, opus 4 / 20, fable 10 / 50. Judge cost per finished task, not per call.
70
71- Split a task that mixes tiers into one agent per tier and fan out. Never send it whole to the strongest tier.
72- A fork always runs on your own model and effort: fork only when the agent truly needs your full context, never for cheap work.
73- Workflow scripts: set model and effort on every agent() call and leave both out only for the final synthesis. Finders over auth, crypto, payments, migrations or raw data access run on opus, because a miss there is never caught downstream.
74- Haiku agents never spawn agents, and agents nest at most two levels deep. The model-routing mod refuses these spawns.
75- A subagent that needs more judgment returns to its parent to re-scope instead of escalating itself. A haiku stage that fails twice moves to sonnet instead of a third try.
76- If the session's own model looks mismatched to the task, say so once and let the user switch with /model.`
77}
78
79/** The fix for an Agent call that names no model. */
80export function missingModelMessage(sessionTier: ModelTier | null): string {
81  const session = sessionTier ?? 'the session model'
82  return `Model routing: set \`model\` and \`effort\` on this Agent call, then call again. Without \`model\` it runs on the session's own model (${session}), the most expensive choice. Pick the cheapest tier whose judgment is enough:
83- haiku, effort low: output fixed by the input plus a template, a wrong result is cheap to spot (renames, short summaries, extracting fields, formatting, run a command and report)
84- sonnet, effort medium: must decide what matters (scoped research, locating code, explaining a subsystem, drafting, a contained change with a clear spec)
85- opus, effort high (xhigh when regressions are costly): judging or verifying findings, design tradeoffs, migrations, auth, payments, data changes
86- ${session} with effort left out: final synthesis that must hold everything in view
87Split a task that mixes tiers into one call per tier.`
88}
89
90function aboveCeilingMessage(requested: ModelTier, sessionTier: ModelTier): string {
91  return `Model routing: this session runs on ${sessionTier}, which caps delegated work. Use ${sessionTier} or a cheaper tier for this agent. If it truly needs ${requested}, tell the user and let them switch the session with /model.`
92}
93
94const HAIKU_SPAWN_MESSAGE =
95  'Model routing: haiku agents do not spawn agents. Finish what you can, and return to your parent with ' +
96  'whatever needs more judgment so it can re-scope.'
97
98const DEPTH_MESSAGE =
99  'Model routing: agents nest at most two levels deep. Do this part yourself, or return to your parent so it ' +
100  'can spawn the work.'
101
102function workflowMessage(sessionTier: ModelTier | null): string {
103  const session = sessionTier ?? 'the session model'
104  return (
105    `Model routing: none of this script's agent() calls set \`model\`, so every agent would run on ${session}. ` +
106    'Set model and effort on each agent() call: scan or transform stages on haiku/low, find or draft stages on ' +
107    'sonnet/medium, verify or judge stages on opus/high. Leave both out only for the final synthesis. Finders ' +
108    'over auth, crypto, payments, migrations or raw data access run on opus.'
109  )
110}
111
112/** True when a workflow script spawns agents but never names a model for any of them. */
113export function workflowLacksModels(script: unknown): boolean {
114  if (typeof script !== 'string') return false
115  return /\bagent\s*\(/.test(script) && !/\bmodel\s*:/.test(script)
116}
117
118// ---------------------------------------------------------------------------
119// Bookkeeping
120// ---------------------------------------------------------------------------
121
122async function sessionTierOf($: Engine): Promise<ModelTier | null> {
123  try {
124    return tierOf(await $.session.model())
125  } catch {
126    return null
127  }
128}
129
130/** Records a spawned agent, keeping the table to its most recent entries. */
131async function rememberAgent($: Engine, agentId: string, agent: ModelRoutingAgent): Promise<void> {
132  await update($, agentsAtom, agents => {
133    const entries = Object.entries({ ...agents, [agentId]: agent })
134    return Object.fromEntries(entries.slice(-MAX_TRACKED_AGENTS))
135  })
136}
137
138// ---------------------------------------------------------------------------
139// Hooks
140// ---------------------------------------------------------------------------
141
142export function registerModelRouting(on: On): void {
143  // The routing guide, whenever Claude can spawn agents.
144  on('prompt.compose', async ($, e, next) => {
145    const composed = await next(e)
146    const offersAgents = e.tools.includes('Agent') || e.tools.includes('Workflow')
147    if (!offersAgents || e.traits.includes('teammate')) return composed
148
149    return {
150      sections: [
151        ...composed.sections,
152        { id: 'model-routing:guide', text: routingGuide(tierOf(e.model)), scope: 'session' },
153      ],
154    }
155  })
156
157  // Every Agent call: a model for agents that have none, an effort for every tier, nothing above the session.
158  on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
159    const agentType = e.subagent_type ?? 'general-purpose'
160    // Forks always run on the caller's model and effort: nothing to route.
161    if (agentType === 'fork') return next(e)
162
163    const sessionTier = await sessionTierOf($)
164    const requestedTier = tierOf(e.model)
165
166    if (requestedTier === null) {
167      // An agent type with no model of its own would run on the session's model.
168      if (UNPINNED_AGENT_TYPES.has(agentType)) return { deny: missingModelMessage(sessionTier) }
169
170      // A typed agent may pin its model: fill the effort only once its tier is known.
171      if (e.effort !== undefined) return next(e)
172      const typeTier = (await read($, typeTiersAtom))[agentType]
173      if (typeTier === undefined || typeTier === sessionTier) return next(e)
174      return next({ ...e, effort: DEFAULT_EFFORT[typeTier] })
175    }
176
177    if (sessionTier !== null && isAboveCeiling(requestedTier, sessionTier)) {
178      return { deny: aboveCeilingMessage(requestedTier, sessionTier) }
179    }
180
181    // A cheaper tier with no effort would inherit the session's: give it its own.
182    // The session's own tier without effort is the final-synthesis case, so it inherits.
183    if (e.effort === undefined && requestedTier !== sessionTier) {
184      return next({ ...e, effort: DEFAULT_EFFORT[requestedTier] })
185    }
186
187    return next(e)
188  }).catch(($, e, next) => next(e)) // Never block work on a fault of this mod.
189
190  // Workflow scripts whose agents would all run on the session's model.
191  on('tool.call', { tool: 'Workflow' }, async ($, e, next) => {
192    if (workflowLacksModels(e.script)) return { deny: workflowMessage(await sessionTierOf($)) }
193    return next(e)
194  }).catch(($, e, next) => next(e))
195
196  // Every spawn, from any source: haiku agents never spawn, and nesting stops at two levels.
197  on('agent.spawn', async ($, e, next) => {
198    if (e.resume !== undefined) return next(e)
199
200    const agents = await read($, agentsAtom)
201    const parent = e.parentAgentId === undefined ? undefined : agents[e.parentAgentId]
202    // An agent spawned before this mod loaded is taken to sit at depth 1.
203    const parentDepth = e.parentAgentId === undefined ? 0 : (parent?.depth ?? 1)
204
205    if (parent?.tier === 'haiku') return { deny: HAIKU_SPAWN_MESSAGE }
206    if (parentDepth + 1 > MAX_SPAWN_DEPTH) return { deny: DEPTH_MESSAGE }
207
208    const spawned = await next(e)
209    if (spawned.deny === undefined && spawned.agentId !== undefined) {
210      const tier = tierOf(spawned.model)
211      await rememberAgent($, spawned.agentId, { tier, depth: parentDepth + 1, type: e.subagentType })
212      // The tier a type runs on when no model is named: its pinned model, or the caller's.
213      if (tier !== null && e.model === undefined && !e.fork) {
214        await update($, typeTiersAtom, typeTiers => ({ ...typeTiers, [e.subagentType]: tier }))
215      }
216    }
217
218    return spawned
219  }).catch(($, e, next) => next(e))
220}
221
types/index.d.ts 26 lines
1// Model routing's shared state: other mods can read which tier each agent runs on.
2
3/** The four model tiers, cheapest first. */
4export type ModelTier = 'haiku' | 'sonnet' | 'opus' | 'fable'
5
6/** One spawned agent as the mod tracks it. */
7export type ModelRoutingAgent = {
8  /** The tier it runs on; null when the model named no known tier. */
9  tier: ModelTier | null
10  /** 1 for an agent the main conversation spawned, 2 for one an agent spawned. */
11  depth: number
12  /** The agent type, like "general-purpose" or "Explore". */
13  type: string
14}
15
16declare module 'claude-code' {
17  interface PluginState {
18    'model-routing': {
19      /** Every agent spawned this session, by agent id. */
20      agents: Record<string, ModelRoutingAgent>
21      /** The tier each agent type ran on when no model was named, by type. */
22      typeTiers: Record<string, ModelTier>
23    }
24  }
25}
26