When the agent’s real cost is I/O

An AI coding agent burns most of its tokens on work that barely qualifies as reasoning. It reads five files to answer a question about a single method. It generates a test that mirrors the twenty tests beside it. It rewrites docs after a meeting. The frontier model handling all of it is wildly overqualified for the job, and every one of those tokens carries a price. By 2028, Gartner predicts AI coding costs will exceed the average developer’s salary; a quarter of engineering leaders already spend $200–$500 per developer per month on tokens, with some past $2,000.

The fix doesn’t require a platform team or another subscription. It’s a routing problem: send the grunt work to a cheap worker model and reserve the expensive one for problems that actually need it.

Two declarative agents

Portal by Spotify’s AiKA Modes are built for exactly this. A mode is a declarative agent running on an ephemeral runtime—think AWS Lambda, but for agents. You define instructions, choose a model, set parameters like temperature, and attach MCP tools. Portal handles infrastructure, API keys, and servers. Modes are callable from the Portal CLI or API and can be public or private.

The router works with two modes, both using Gemini 2.5 Flash as the worker model in the examples below. The model field accepts anything configured in your Portal instance.

Bulk-reader

Handles the case where Claude would otherwise read several large files just to answer one question.

name: bulk-reader description: Bulk file reader for code analysis - delegates I/O from Claude Code instructions: You are a precise code analyst. Read the provided files and answer the question concisely. Output structured bullets only. No greetings, no prose, no preambles. Lead every bullet with the exact name, type, or line number. Use nested bullets for details. Skip anything the caller did not ask for. visibility: public model: gemini-2.5-flash resourceLimits:   temperature: 0.2 tags:   - coding   - delegation

Code-writer

For tests, config scaffolding, type stubs, or anything whose output is predictable from existing patterns.

name: code-writer

description: Boilerplate code generator - delegates output-heavy work from Claude Code

instructions: You generate code files based on a spec and reference files. Match the existing patterns, conventions, naming, and style exactly. Output only the code — no explanations, no markdown fences unless asked. If the spec is ambiguous, make reasonable choices that match the reference code's patterns.

visibility: public

model: gemini-2.5-flash

resourceLimits:
  temperature: 0.2

tags:
  - coding
  - delegation

The “output only the code” instruction matters: without it, the model wraps everything in markdown fences and prose that Claude then has to parse.

Enforcing the route with a plugin

The first routing attempt was a block of advisory rules in CLAUDE.md. Claude could ignore them, and every project needed its own copy. The current version is a Claude Code plugin called shunt, which delegates through the Portal CLI actions registry so it works against any Portal instance with AiKA enabled.

Hooks block expensive reads

Claude Code hooks fire before every tool call. Shunt registers two PreToolUse hooks. check-file-size fires on every Read call; if the file exceeds a configurable line threshold (default: 350), the hook blocks the read and directs Claude to the /bulk-reader skill. Targeted reads with known offsets pass through. check-bash-read catches cat, head, tail, less, and more on large files—piped commands like cat file | grep are treated as targeted and pass. The threshold is adjustable via the SHUNT_MIN_LINES environment variable in your shell profile or .claude/settings.json:

{   "env": {     "SHUNT_MIN_LINES": "500"   } }

Scripts wrap the CLI calls

Two bash scripts wrap the Portal CLI. Claude calls them with named arguments; the scripts build the request, invoke the action, unwrap errors, and report token usage to stderr. Modes are resolved by Portal case-insensitively, preferring your own mode, then your team’s, then public ones. Fork a public mode and yours automatically takes precedence.

bulk-read wraps each file in XML tags for boundaries and sends everything to the bulk-reader mode with the question.

bulk-read --question "What does this service do?" --paths src/Service.java src/Handler.java

# Follow-up: ask again with the same paths

bulk-read --question "Which methods call the database?" --paths src/Service.java src/Handler.java

Each delegation is one shot: invocation is ephemeral, nothing is stored server-side. Follow-ups are free where it matters because the corpus goes to the worker model and never enters Claude’s context.

code-write sends a spec and a reference file to the code-writer mode, strips markdown fences, and can write directly to disk so Claude never sees the generated code. The reference file is mandatory—without patterns to match, the worker produces context-free code that fits nothing in the project.

code-write --spec "Write tests for UserService" --reference tests/OrderTest.java --target tests/UserTest.java

# Output to stdout

code-write --spec "Generate a config stub" --reference config/existing.yaml

Skills bridge the gap

Two skill files tell Claude when and how to call the scripts. When a hook blocks a read, the block message points Claude to the skill with the exact invocation syntax. The system degrades gracefully: even if Claude ignores the skill, the hook still blocks the expensive read—the skill just makes the redirect smoother.

Measured savings and known limits

Testing against a Java monorepo across four scenarios measured what Claude would consume reading files directly versus consuming the bulk-reader’s summary. Mean savings were around 90%. The code-write scenario is harder to quantify because without shunt, Claude both reads references and pays expensive output-token rates to generate code; with shunt, the code goes straight to disk.

Delegation has boundaries. You can’t delegate editing—worker summaries lack reliable line numbers, so Claude still reads the specific section for targeted edits. You can’t delegate reasoning—the worker missed a subtle thread-safety bug that Claude caught in seconds, so debugging, architecture, and safety-critical code stay with the frontier model. Latency also adds up: each delegation is a network round-trip that typically takes 10–30 seconds, and Portal caps a single invocation at 30 seconds, forcing very large generations to be split. Below the line threshold, the overhead exceeds the savings.

Routing as a configuration problem

The plugin is Claude-specific, but the pattern—model routing powered by AiKA modes—is the load-bearing piece. Modes are reusable across projects and tools that can shell out to the Portal CLI, shareable publicly, and composable: a doc-writer for documentation, a reviewer for summaries, a translator for i18n. They also decouple the routing decision from the worker. The plugin decides when to delegate; the mode decides how to respond. Swap Gemini Flash for a cheaper model, change the system prompt, add MCP tools—the plugin doesn’t change.

That turns model routing from a systems engineering problem into a configuration problem. You describe what you want and name it.

Running it yourself

  1. Install both plugins from the spotify/portal-ai-plugins marketplace:

    1. claude plugin marketplace add spotify/portal-ai-plugins
    2. claude plugin install portal@portal
    3. claude plugin install shunt@portal
  2. In a new Claude Code session, run /portal:setup to authenticate the Portal CLI against your Portal instance. The portal plugin provides the CLI that shunt delegates through.

  3. Ask a question that spans multiple files. The bulk-reader and code-writer modes are already public—fork them in Portal to customize the worker model or instructions, and your version takes precedence automatically.