Introduction
A few months into daily AI coding, most engineers get the same shock: the bill. Not the seat license — the tokens. A quarter of engineering leaders already burn hundreds of dollars per developer per month, and the meter runs on every file the agent reads.
Here is the liberating insight, popularized recently by Spotify's Portal team: most of what your coding agent consumes isn't thinking. It's I/O. Reading five 2,000-line files to answer one question. Generating a test file that copies the twenty test files next to it. Thousands of tokens, near-zero reasoning — all billed at frontier-model prices.
This article shows you a setup I built for the opencode coding agent that fixes exactly that: grunt work goes to cheap models, reasoning stays on the strong model. No subscription, no backend, no new infrastructure. Everything runs locally.
Tokens in 60 Seconds
If you are new to this: models charge per token (roughly 3–4 characters of text). Two prices matter:
- Input tokens — everything the model reads (your prompt + every file it opens). This is where bills explode.
- Output tokens — what the model writes. Usually priced higher per token, but smaller in volume.
And models come in tiers. A frontier model (the smartest) can cost 10–20x more per token than a small "flash"/"lite"/"nano" model. The small ones are worse at reasoning — but reading a file aloud and summarizing it barely needs reasoning. That price gap is the entire opportunity.
The Core Idea: Route by Job, Not by Habit
Today, one expensive model does everything:
Expensive model: reads 4,000 lines + thinks + writes answer
= 4,000 expensive input tokensWith routing:
Hook (free code): blocks the 4,000-line read = 0 tokens
Cheap model: reads 4,000 lines, returns 30-line
summary = 4,000 cheap tokens
Expensive model: reasons over the 30-line summary = 30 expensive tokensSame answer. A fraction of the expensive tokens. The idea generalizes:
Three layers make it work: enforcement (a hook that blocks wasteful reads), routing (instructions for when to delegate), and workers (cheap subagents that do the I/O). Let's build each one.
Piece 1: The Hook (Enforcement)
Instructions alone don't work — models ignore advisory rules when it's convenient. So the first piece is deterministic code, not AI: a plugin hook that fires before every file read, counts lines, and refuses reads that are too big. Zero tokens, impossible to sweet-talk.
In opencode, plugins are JavaScript files with a tool.execute.before event. Here is the complete hook (behavior adapted from Spotify's open-source shunt plugin):
import fs from "node:fs";
const MIN_LINES = parseInt(process.env.SHUNT_MIN_LINES ?? "350", 10) || 350;
function countLines(path) {
try {
return fs.readFileSync(path, "utf8").split("\n").length;
} catch {
return 0; // missing/unreadable → let the read tool report the error
}
}
export const ShuntPlugin = async () => {
return {
"tool.execute.before": async (input, output) => {
if (process.env.SHUNT_DISABLE === "1") return; // kill switch
if (input.tool !== "read") return;
const { filePath = "", offset = null, limit = null } = output.args ?? {};
// Targeted reads ALWAYS pass: the agent already knows what it needs.
if (offset != null || limit != null) return;
if (!filePath) return;
const lines = countLines(filePath);
if (lines > MIN_LINES) {
throw new Error(
`File is ${lines} lines (threshold: ${MIN_LINES}). ` +
`Do NOT read it directly. Delegate to the @bulk-reader ` +
`subagent with your question and the file path instead. ` +
`If you need exact content for editing, re-read with ` +
`offset/limit for just the section you need.`
);
}
},
};
};Read it slowly — every branch is a deliberate decision:
| Branch | Why |
|---|---|
offset/limit set → allow | Targeted reads are already cheap and precise; blocking them would make editing impossible |
| Missing file → allow | Not our problem — the read tool itself reports the error |
| Under threshold → allow | Below ~350 lines, the extra round-trip costs more than it saves |
| Over threshold → throw | The redirect names the delegate AND the escape hatch, so the agent always knows its next move |
A companion check does the same for cat, head, tail, less, and more in shell commands (agents love sneaking big reads through bash), while letting piped commands like cat file | grep through — those are already targeted.
Piece 2: The Cheap Workers
Blocking reads is only half the job — someone still has to do the reading. That someone is a subagent pinned to a cheap model. In opencode, a subagent is a markdown file with frontmatter. The model: line is the entire cost trick:
---
description: Bulk file reader for code analysis. Delegate to me instead of
reading files over ~350 lines directly when you need to UNDERSTAND code,
not edit it.
mode: subagent
model: opencode/gemini-3.5-flash-lite
temperature: 0.2
permission:
edit: deny
bash: deny
---
You are a precise code analyst. Answer questions about the provided files.
Read large files in windows (offset/limit, max ~300 lines per read).
Output structured bullets only — no greetings, no prose.
Lead every bullet with the exact name, type, or line number.
NEVER invent line numbers: flag anything uncertain and tell the caller
to verify with a targeted read before editing.Three things to notice:
descriptionis load-bearing. The main agent auto-delegates based on it — write it as "when to use me" instructions, not a biography.edit: deny, bash: denymakes it physically read-only. Even a confused cheap model cannot modify your code. Removed capability beats careful prompting — this is your main anti-hallucination guarantee.- Windowed reads dodge the hook. The subagent reads in ≤300-line windows, so its own reads pass the 350-line gate. No special-casing, no bypass lists, no infinite redirect loops — just arithmetic.
The second worker generates boilerplate (tests, stubs, configs) with one strict rule: a reference file is required, and it refuses security-sensitive work (auth, crypto, migrations). Pattern-matching is cheap; judgment stays expensive.
My current model map — no OpenAI or Anthropic anywhere:
| Role | Model | Rationale |
|---|---|---|
| Complex reasoning | glm-5.3 | Primary driver |
| UI work only | kimi-k3 (scoped subagent) | Frontend specialist |
| Bulk reads | gemini-3.5-flash-lite | Cheapest tier; reading is the easiest job |
| Boilerplate | glm-5.3-flash | One step up; generation needs more care |
Piece 3: The Routing Policy
The hook covers reads. Everything else — should this test be delegated? is this debugging? — is judgment, so it lives in plain instructions (AGENTS.md):
DELEGATE to @bulk-reader: understanding questions over large files.
DELEGATE to @code-writer: tests/stubs/configs when a reference file exists.
DELEGATE to @ui-builder: all UI/frontend work.
NEVER delegate: edits needing exact lines, debugging, architecture
decisions, auth/crypto code, migrations, secrets.
VERIFY: line numbers from @bulk-reader with a targeted read before editing.Degrades gracefully: even if the main agent skims this, the hook still blocks the expensive read. The policy just makes the redirect smoother.
Proving It Works: Deterministic Tests
You don't need to burn tokens to trust routing logic. The hook is pure functions (file path in, allow/block out), so it gets unit tests with temporary fixture files — no network, no models, no cost:
// 2000-line file, full read → blocked
decideRead({ filePath: large }) // { decision: "block", reason: "…" }
// same file, windowed read → allowed
decideRead({ filePath: large, offset: 100, limit: 50 }) // { decision: "allow" }
// cat with a pipe → allowed (already targeted)
decideBash({ command: "cat large.txt | grep foo" }) // { decision: "allow" }Thirty-plus cases like these — boundaries (exactly 350 lines passes, 351 blocks), missing files, quoted paths, flag stripping (head -100), kill switch — run in milliseconds under node --test. Routing you can prove beats routing you hope for.
Measure Your Savings (Don't Trust Headlines)
Published "90% savings" numbers come from bulk-read benchmarks, not whole workloads. Your number depends on your mix of reading vs. reasoning. Measure it:
- Baseline a read-heavy task, note the session token count.
- Re-run it in a fresh session with routing on. Compare primary-model input tokens.
- Tune the threshold down (500 → 350 → 250) until savings flatten or latency annoys you — each delegation is a round-trip.
Honest Limitations
- Delegated summaries lack reliable line numbers. Edits always need a direct targeted re-read first.
- Cheap models miss subtle bugs. Debugging and architecture stay on the frontier model, always.
- Latency adds up. Each delegation is a round-trip; tiny files aren't worth routing.
- If your work is mostly reasoning, savings will be modest. Routing taxes I/O, not thought.
Key Takeaways
- Most agent spend is I/O, not intelligence — route by job, not by habit.
- Enforcement must be deterministic code (a hook), not instructions models can ignore.
- Cheap workers need fenced permissions (read-only, reference-required), not just careful prompts.
- Prove routing with unit tests; prove savings by measuring your own workload.
- The whole setup is local files — a plugin, two markdown agents, a policy snippet. No subscription, no backend, and you can revoke it by deleting files.
The complete setup described here — plugin, agents, skills, tests, and an installer — lives in a private project of mine; every pattern above is reproduced in full so you can rebuild it from this article alone. The routing concept is credited to Spotify's open-source portal-ai-plugins/shunt, re-implemented here for opencode with the paid Portal backend replaced by local subagents.