Not every step needs the expensive model
Tier selection is not a preference, it is a function of three facts about the task. Two platform constraints rule out most of the combinations you would guess.
A commit across three repositories. Stage, check a submodule pointer direction, commit, push, each level before the one above it. The order never varies and there is nothing to decide.
Dispatching an agent to reason through it cost about 180,000 tokens and seven and a half minutes. The shell script that does the same work takes seconds and spends nothing.
The case this post is about
One command, /git-session-commit, with two implementations that both exist:
core/scripts/git-session-commit-lite.sh pure bash, ~0 tokens, seconds
layers/git/commands/git-session-commit.md the full runtime, dispatched to a model
The script does PR discovery, a bottom-up walk of source → workspace → framework, the submodule-pointer direction check, and the session-binding write. That is not a simplified version of the runtime — it is the runtime's own logic, in bash.
The incident is that for a routine commit the model path was taken anyway.
The symptom
The visible shape is the bill, and it arrives as a surprise because nothing about the invocation looked expensive. A commit is a small operation. The tokens went on reading the runtime document, the rules it loads, the repository state, and then narrating a sequence that was fully determined before it started.
The second shape is that it was slower in wall clock, which is the part people do not predict.
Seven and a half minutes against a few seconds, for work whose bottleneck is three git push
calls. The model was not thinking about anything hard; it was reading.
The third shape is the one that should decide the argument: it was less reliable. A fixed sequence re-derived each time is a sequence that can come out differently. The script cannot forget the bottom-up order, cannot skip the pointer-direction check, and cannot decide that staging everything at the top level is equivalent. A model doing it from a document can do all three, and each of those is a defect with a name in this repository because each has happened.
The fourth is the inverse failure, and it is why this is a tiering problem rather than an argument for scripts. Send a task that genuinely needs judgement to the cheap tier and you get a confident wrong answer at speed. The read-only auditor that classifies a finding, the reviewer that decides whether a handler is a stub — those are not scriptable, and a tier chosen for cost produces plausible output with nothing behind it.
Why the obvious fix failed
Use the cheap tier by default and escalate. The intuition, and it breaks on what escalation costs. Discovering mid-task that the tier was wrong means the cheap attempt was wasted and the expensive one starts cold — and worse, the cheap tier does not reliably know it is out of its depth. The failure mode is not "I cannot do this", it is an answer.
Pick by how important the task feels. This is what actually happens in the absence of a rule, and importance is uncorrelated with what the tier needs to be. Promoting a release to production is about as important as an operation gets and is almost entirely mechanical. Classifying one lint finding is unimportant and needs judgement.
Declare the tier in the command's metadata and let the platform route it. We did this, with a
model: field in each command's frontmatter. It worked until it did not: on a 1M-context session
the platform forks a declared model: onto a separate extra-usage variant, so every routable
command started failing with a usage-credits error. The fix was to remove the declaration and put
the dispatch in the command body as an explicit instruction — which means the routing decision is
in the thing being routed, not in metadata a platform interprets differently by context.
Let the model choose its own tier. Appealing and circular. Choosing requires knowing the shape of the task, which is most of the reasoning, and a model that has done that reasoning has already spent the expensive tier's tokens.
The actual mechanism
Tier is a function of three facts about the task, and two platform constraints rule out most of the combinations you would otherwise reach for.
The three facts: does it mutate files, does it need judgement, and does it dispatch other agents.
The two constraints are the load-bearing part, because they are not preferences:
Subagents cannot spawn subagents. So anything that orchestrates must run in the main session. An orchestrator dispatched to a subagent is an orchestrator that cannot orchestrate, and the failure is not an error — it is an agent that does the work itself, serially, having been designed to fan out.
The cheap read-only tier has read-only tools. Read, search, list, shell. So anything that writes a file cannot go there regardless of how mechanical it is. This one is easy to get wrong because the task looks cheap: reformatting a registry file is trivial work, and the tier that should obviously handle trivial work physically cannot.
What falls out is a four-way classification where one option — run it in the main session — absorbs the large majority, because orchestration and interactivity are both common and both require it.
The fix
The classifier is a few branches, and the order matters: the constraints are checked before the preferences, because a constraint violated is a broken command and a preference violated is just expensive.
type Task = {
name: string;
mutatesFiles: boolean;
needsJudgement: boolean;
dispatchesAgents: boolean;
isInteractive: boolean;
isFullyDeterministic: boolean; // a script could do it with no judgement at all
};
type Tier =
| "script" // no model at all
| "dispatch-haiku-readonly"
| "dispatch-sonnet-write"
| "inline-opus"; // the main session
function tierFor(t: Task): { tier: Tier; why: string } {
// 0. Not a model problem. Checked FIRST, because the cheapest tier is no tier —
// and this is the branch the 180k-token commit should have taken.
if (t.isFullyDeterministic && !t.needsJudgement) {
return { tier: "script", why: "fixed sequence, no judgement — a script is faster AND cannot forget a step" };
}
// 1. CONSTRAINT, not preference: subagents cannot spawn subagents.
if (t.dispatchesAgents) {
return { tier: "inline-opus", why: "orchestrates — a subagent cannot spawn subagents" };
}
// 2. CONSTRAINT: interactive work needs the session that can talk to the operator.
if (t.isInteractive) {
return { tier: "inline-opus", why: "interactive — only the main session can render a picker" };
}
// 3. CONSTRAINT: the read-only tier has no write tools, however cheap the work looks.
if (t.mutatesFiles) {
return { tier: "dispatch-sonnet-write", why: "mutates files — the readonly tier has no write tool" };
}
// 4. Only now is it a preference, and the answer is the cheap tier.
return t.needsJudgement
? { tier: "dispatch-sonnet-write", why: "read-only but needs judgement" }
: { tier: "dispatch-haiku-readonly", why: "read-only, mechanical — cheapest tier that can do it" };
}
And the check that catches a declaration drifting from what the command actually does, which is the half that keeps the classification honest:
function auditDeclared(t: Task, declared: Tier): string | null {
const { tier, why } = tierFor(t);
if (tier === declared) return null;
// A WRONG tier is not a cost problem. These two are broken commands.
const broken =
(t.dispatchesAgents && declared !== "inline-opus") ||
(t.mutatesFiles && declared === "dispatch-haiku-readonly");
return `${t.name}: declared ${declared}, computes to ${tier} (${why})` +
(broken ? " ← BROKEN, not merely mispriced" : " ← mispriced");
}
The commit case and three others, run through it:
[
{ task: { name: "git-session-commit", mutatesFiles: true, needsJudgement: false,
dispatchesAgents: false, isInteractive: false, isFullyDeterministic: true },
declared: "inline-opus" as Tier },
{ task: { name: "idea-agent", mutatesFiles: true, needsJudgement: true,
dispatchesAgents: true, isInteractive: false, isFullyDeterministic: false },
declared: "dispatch-sonnet-write" as Tier },
{ task: { name: "verify-coherence", mutatesFiles: false, needsJudgement: false,
dispatchesAgents: false, isInteractive: false, isFullyDeterministic: false },
declared: "dispatch-haiku-readonly" as Tier },
{ task: { name: "registry-format", mutatesFiles: true, needsJudgement: false,
dispatchesAgents: false, isInteractive: false, isFullyDeterministic: false },
declared: "dispatch-haiku-readonly" as Tier },
].forEach(({ task, declared }) => console.log(auditDeclared(task, declared) ?? `${task.name}: ok`));
git-session-commit: declared inline-opus, computes to script (fixed sequence, no judgement —
a script is faster AND cannot forget a step) ← mispriced
idea-agent: declared dispatch-sonnet-write, computes to inline-opus (orchestrates — a subagent
cannot spawn subagents) ← BROKEN, not merely mispriced
verify-coherence: ok
registry-format: declared dispatch-haiku-readonly, computes to dispatch-sonnet-write
(mutates files — the readonly tier has no write tool) ← BROKEN, not merely mispriced
The distinction in that output is the point. Line one is the 180,000-token commit: a real cost, a real mistake, and the command worked. Lines two and four are commands that cannot work — an orchestrator that cannot fan out, and a file mutator with no write tool — and they would both have read as mysterious behaviour rather than as a routing error.
How we proved it
The distribution across 396 audited commands:
358 inline-opus
19 dispatch-sonnet-write
16 dispatch-haiku-readonly
1 dispatch-opus-write
Ninety percent in the main session, which looked wrong to me when I first counted it and is correct.
The two constraints explain it: orchestration forces inline-opus, interactivity forces
inline-opus, and this framework is mostly orchestrators and interactive workflows. The commands
that are neither — a read-only audit, a scan, a formatter — are the 35 that route.
The cost measurement is one data point and it is a large one. ~180,000 tokens and ~7.5 minutes for a routine three-repository commit, against a script that completes in seconds for no tokens. That is a ratio large enough that precision does not matter; what matters is that the expensive path was also the slow path and the less reliable one, so there is no trade being made.
What I have not measured is the aggregate. I know what one mis-tiered command cost once. I do
not have a figure for what the 358 inline-opus commands cost in total versus what they would cost
if more of them could route, because most of them cannot route — the constraints are not
negotiable, so the counterfactual does not exist.
What I got wrong twice. First, I put the tier in frontmatter and let the platform route it, which broke on 1M-context sessions with a usage-credits error — the declaration caused a fork onto a separate billing variant. Second, when I moved dispatch into the command body, I did not immediately add the audit above, so three commands drifted from their declared tier before anything noticed. The classifier is only useful paired with a check that runs it against what is declared.
And the honest limit on the script tier: isFullyDeterministic is a judgement I am making about
the task, by hand, and nothing verifies it. A sequence I believe is fixed may have a branch I have
not hit. The guard against that is that the script's own failure is loud — it exits non-zero and
stops — rather than that my classification is reliable.
What this generalizes to
The useful axis for allocating an expensive resource across a set of tasks is rarely importance and rarely cost. It is what the task structurally requires, checked before any preference about what it deserves.
Two things follow from putting the constraints first. The cheapest tier turns out to be no model at all, and it is the branch that gets skipped because the question is framed as "which model" rather than "does this need one". And a tier chosen in violation of a constraint is not an expensive mistake — it is a broken capability whose symptom looks like bad behaviour, which is a much harder thing to diagnose than a bill.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".