SLOPSHOPPER

cost-router

Status line with 5h usage and context %, toasts at 70/85% usage, and keyword-based model choice for unpinned subagents.

newguardtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cost-router
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ cost-router: 5h 31% | ctx 49%
README

cost-router

Usage in the status line, spend warnings, cheaper models for subagents.

Shows 5h NN% | ctx NN% in the status line, toasts once when your 5-hour usage passes 70% and 85%, and picks haiku/sonnet/opus for subagents that were given no model, based on keywords in their task. Explicit models are never overridden.

Install

/plugin marketplace add cloudcruncher/claude-mod-garage
/plugin install cost-router@mod-garage

Run /reload-plugins (or restart Claude Code) afterwards. To try it from a local clone without installing: claude --plugin-dir ./dev-mods/cost-router

Use it

  • Automatic once installed.

What you'll see

Illustrative output.

Status line

5h 62% | ctx 34%

Toast

5h usage passed 70% (now 71%)

Notes

  • With no subscription rate-limit data, values show as n/a and no toasts fire.
Source 2 files
hooks/register.ts 27 lines
1import type { Register } from 'claude-code'
2import { crossed, pickTier, statusLine } from './route'
3
4const THRESHOLDS = [70, 85] as const
5
6export const register: Register = on => {
7  const fired = new Set<number>()
8
9  on('session.measure', ($, e, next) => {
10    const five = e.rateLimits.find(r => r.kind === 'five_hour')?.percentUsed
11    $.ui.status(statusLine(five, e.context.percent))
12    if (five !== undefined) {
13      for (const t of crossed(five, THRESHOLDS, fired)) {
14        fired.add(t)
15        $.ui.toast(`5h usage passed ${t}% (now ${Math.round(five)}%)`)
16      }
17    }
18    return next(e)
19  })
20
21  on('tool.call', { tool: 'Agent' }, ($, e, next) => {
22    if (e.model !== undefined) return next(e)
23    const tier = pickTier(`${e.description ?? ''} ${e.prompt ?? ''}`)
24    return tier ? next({ ...e, model: tier }) : next(e)
25  })
26}
27
hooks/route.ts 25 lines
1export type Tier = 'haiku' | 'sonnet' | 'opus'
2
3const OPUS = /\b(review|audit|security|vulnerab\w*|architect\w*|design|trade-?offs?|threat|plan(ning)?)\b/i
4const SONNET = /\b(implement\w*|fix\w*|bug|test(s|ing)?|refactor\w*|build|write|add|migrat\w*|debug\w*|update|create)\b/i
5const HAIKU = /\b(search|find|look ?up|locate|list|grep|summari[sz]e|summary|explore|triage|read|check|where)\b/i
6
7/** Pick a tier from the delegated prompt; opus beats sonnet beats haiku. Undefined when nothing matches. */
8export function pickTier(text: string): Tier | undefined {
9  if (OPUS.test(text)) return 'opus'
10  if (SONNET.test(text)) return 'sonnet'
11  if (HAIKU.test(text)) return 'haiku'
12  return undefined
13}
14
15/** Crossed thresholds (ascending) between the previous and current percent, not already fired. */
16export function crossed(percent: number, thresholds: readonly number[], fired: ReadonlySet<number>): number[] {
17  return thresholds.filter(t => percent >= t && !fired.has(t))
18}
19
20export function statusLine(fiveHour: number | undefined, context: number | undefined): string {
21  const f = fiveHour === undefined ? '5h n/a' : `5h ${Math.round(fiveHour)}%`
22  const c = context === undefined ? 'ctx n/a' : `ctx ${Math.round(context)}%`
23  return `${f} | ${c}`
24}
25