Inspired by Spotify Engineering's approach to model routing. Read the original: Portal by Spotify cut my Claude Code token usage by 90%
Most of what Claude Code does for me is not reasoning. It is I/O.
Reading five files to answer a question about one method. Generating a test file that mirrors the pattern of twenty test files sitting right next to it. Updating docs after a sync. Thousands of tokens gone, almost zero actual thinking. The seat license is not the problem. The tokens are.
I came across Spotify Engineering's write-up on Portal and AiKA Modes and decided to test it on a real backend codebase. The numbers held. Around 90% token reduction on file-heavy tasks. This is how it works and why the architecture makes sense.
The core problem
Frontier models like Claude are overqualified for most coding I/O work. You do not need a model trained on the sum of human knowledge to read a Java service file and summarize what it does. But Claude reads it anyway, at frontier token cost, every single time.
By 2028, AI coding costs are projected to exceed the average developer salary. A quarter of engineering leads already spend $200 to $500 per developer per month on tokens. Some are past $2,000. The tooling pays for itself only if you stop routing grunt work to expensive models.
Two modes, no infrastructure
Portal by Spotify introduced AiKA Modes: declarative agents that run on ephemeral runtimes. You define instructions, pick a model, set temperature, attach MCP tools if needed. No API keys to manage, no long-running servers. Modes are callable from the Portal CLI or API.
I set up two.
Mode 1: bulk-reader
Handles the case where Claude would otherwise read multiple large files just to answer one question.
yaml
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
- delegationMode 2: code-writer
Handles tests, config scaffolding, type stubs, anything where the output is predictable from existing patterns.
yaml
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
- delegationThe "output only the code" instruction in code-writer matters. Without it, the model wraps everything in markdown fences and explanatory prose that Claude then has to parse through and pay for.
Both use Gemini 2.5 Flash as the worker model. The model field accepts anything configured in your Portal instance.
The routing layer
The first version was a block of routing rules in CLAUDE.md. It sort of worked. Claude would read the instructions and self-route to Portal. But the rules were advisory, not enforced. Claude could ignore them. Every project needed its own copy.
The production version is a Claude Code plugin called shunt. Three layers.
Layer 1: Hooks
Hooks fire before every tool call.
check-file-size fires on every Read call. If the file exceeds a configurable line threshold (default: 350 lines), the hook blocks the read and tells Claude to use the /bulk-reader skill instead. Targeted reads with offset/limit pass through since Claude already knows the section it needs.
check-bash-read catches cat, head, tail, less, and more on large files. Piped commands like cat file | grep pass through since those are targeted reads.
Set the threshold in your shell profile or .claude/settings.json:
json
{
"env": {
"SHUNT_MIN_LINES": "500"
}
}Layer 2: Scripts
Two bash scripts wrap the Portal CLI calls. Claude calls a script with named arguments. The scripts handle building the request, invoking the actions, unwrapping errors, and reporting token usage to stderr.
bulk-read wraps each file in XML tags for clear boundaries and sends them to the bulk-reader mode with the question:
bash
bulk-read --question "What does this service do?" --paths src/Service.java src/Handler.java
# Follow-up reuses same paths, corpus goes to the worker, never enters Claude's context
bulk-read --question "Which methods call the database?" --paths src/Service.java src/Handler.javaEvery delegation is one shot. Nothing is stored server-side. Re-sending files on a follow-up is free where it matters because those tokens go to the cheap worker model, not Claude.
code-write sends a spec and a reference file to code-writer, strips markdown fences from the output, and writes directly to disk. Claude never sees the generated code.
bash
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.yamlThe reference file is required. Without a file to pattern-match against, the worker generates context-free code that fits nothing in your project.
Layer 3: Skills
Two markdown skill files tell Claude when and how to call the scripts. When the hook blocks a read, the block message points Claude to the /bulk-reader skill, which shows the exact invocation syntax.
This degrades gracefully. Even if Claude does not read the skill description, the hook still blocks the expensive read. The skill just makes the redirect cleaner.
The numbers
Tested across a Java monorepo, four scenarios. Mean bulk-read token savings: around 90%.
The code-write scenario is harder to measure cleanly because without shunt, Claude both reads reference files and generates output as expensive output tokens. With shunt, the code goes straight to disk and Claude never touches it.
What does not work
You cannot delegate editing. The worker model's summaries do not include reliable line numbers. If Claude needs to make edits based on an analysis, it still reads the specific section directly. The hooks allow targeted reads for exactly this reason.
You cannot delegate reasoning. In testing, the worker found surface-level patterns but missed a subtle thread-safety bug. Claude caught it immediately once given the right context. The routing explicitly excludes debugging, architectural decisions, and safety-critical code.
Latency adds up. Each delegation is a network round-trip: Claude Code to Portal backend to worker model and back. Responses typically take 10 to 30 seconds. Portal caps a single invocation at 30 seconds, so very large generations need splitting into smaller calls. Below the line threshold, delegation overhead exceeds the savings.
Why this architecture holds
The modes are the load-bearing piece, not the plugin.
The same bulk-reader and code-writer modes work across every project and every tool that can shell out to the Portal CLI. Both are public in AiKA and usable today without creating your own copies.
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 does not change.
You could extend this further: a doc-writer mode for documentation, a reviewer mode for code review summaries, a translator mode for i18n. Each one is a few lines of config.
Model routing is normally a systems engineering problem. AiKA Modes turn it into a configuration problem.
Original research and implementation by Spotify Engineering. This post documents my own experience applying the same approach to a production backend codebase.
Reference: Portal by Spotify cut my Claude Code token usage by 90%