Model tiers
A job row can say which model its worker runs on. A model tier names a rung of
that choice in your own words, cheapest first, so a row, a rule or a report says
economy instead of a model id that changes every few months. The magusfile
declares the tiers once. magus records the tier on each job when it is forked and
never changes it afterward.
magus does not route anything. It never picks a model for a call, retries a job at a dearer tier or switches a worker mid-run. A tier changes only when someone edits the row, the matrix function or the declaration, which keeps the choice of how much to spend with a human in the loop.
What a workspace gets by default
With no declaration, the workspace uses the magus/modeltiers module: economy,
standard and principal, with standard as the default. Economy workers are
capped at 100000 tokens of context and load the full skill bodies, since a smaller model is the reader least able to
re-derive the steps the short form leaves out. No tier caps how many of its jobs
are live at once; a cap applies only where a magusfile declares one. The module
names no model, and
the default maps no harness to one: until a magusfile maps a harness with
hosts, below, a tier has no model alias for any harness.
magus\job.modelTiers(null) turns tiers off.
Declare your own
Copy the module into the root magusfile and change what you need:
import "magus";
import "spells/harness/claude-code" as claudecode;
enum ModelTier { economy, standard, principal }
fun claudeCode(tier: ModelTier) > str {
return match (tier) {
.economy -> "haiku",
.standard -> "sonnet",
.principal -> "opus",
};
}
fun bounds(tier: ModelTier) > magus\ModelTierBounds {
return match (tier) {
.economy -> magus\ModelTierBounds{ maxContext = 100000 },
.standard -> magus\ModelTierBounds{},
.principal -> magus\ModelTierBounds{ maxLive = 1 },
};
}
fun skills(tier: ModelTier) > magus\SkillVariant {
return match (tier) {
.economy -> magus\SkillVariant.full,
.standard -> magus\SkillVariant.short,
.principal -> magus\SkillVariant.short,
};
}
final cc = magus\harness.provider(claudecode);
magus\job.modelTiers(ModelTier, default: ModelTier.standard,
hosts: {cc: claudeCode},
bounds: bounds, skills: skills);
- The enum is yours. Its order is the rank, cheapest first. A misspelled
tier fails
magus check, like any other enum. - Every mapping is a
matchover the enum. Add a tier andmagus checkfails untilhosts,boundsandskillseach cover it. hostsis keyed by harness.magus\harness.providerreturns the harness id; keep it, asccabove, and use it as the key. A call that ignores the return value wires the harness all the same. The harness must be wired above the declaration. The function answers the model alias that harness spawns for the tier, such as the stringsonnet. Two tiers cannot share an alias on one harness, because a spawn of that alias would name no one tier.boundscaps a tier.maxLiveis how many jobs at the tier may be live at once andmaxContextthe tokens a worker at the tier may reach. A field left out is no cap.skillspicks the skill variant the tier's workers load:shortorfull.
magus calls each function once per tier while the magusfile loads, and keeps none of them. A declaration it cannot use stops the load with MGS1052: tiers that are not an enum, a default that is not one of its cases, a host the magusfile did not wire, a function that raises or answers the wrong type, a second call, or a call from a project's magusfile instead of the root one.
Which tier a job gets
The store assigns the tier when a row is created or its model changes, whichever
door writes it: magus job fork, magus job apply, or magus\job.put from a
magusfile or the client MCP tool. It tries these in order and records which step
decided:
- The row. A
modelthat is a tier name wins (row), and so does amodelthat a harness'shostsfunction maps to a tier, such asopus: the job takes that tier and keeps the model. An alias that two harnesses map to different tiers fails the fork, since it names no one tier. Amodelthat names no tier and no alias is kept as the row's word and the job takes the declared default's rank; hosts name their models differently, so the fork records the mismatch in the reason instead of refusing it.magus job fork --modeltakes either. - The matrix. The function registered with
magus\job.modelTier, when the row names no model (matrix). It may decline withnull. - The default. The declaration's default, with the skill variant the
declaration gives it (
default). A workspace that declares nothing records the shipped default.
The record holds the tier, its rank, the skill variant, the source, the reason and,
for a matrix, the magusfile that registered it. magus job fork prints it:
model tier: economy (rank 0), matrix: "1 file" at magusfile.buzz
magus describe job prints the same line and the skill variant under it, and
magus ls jobs shows the tier in the MODEL column of a row that names no model.
Editing a row without changing its model leaves the tier alone, and nothing
re-runs the matrix: a tier changes when someone edits the row, the matrix or the
declaration.
A fork that would put more jobs at a tier than its maxLive allows is refused
before anything is written, and the message names the bound. The count is the
live jobs recorded at that tier, read inside the write, so two forks racing for the
last place cannot both take it. A matrix that raises, or answers with a tier the
declaration lacks, a skill variant that is not one, or no reason, fails the fork;
it never falls through to the default.
magus job apply --dry-run assigns nothing and does not run the matrix.
Pick a tier with a matrix
The matrix is a function of the job's declared record and the workspace's
recorded history. The record's writeProjects are the projects that own the
job's write paths, found the way magus describe job finds a job's projects.
history holds one record per tier and job kind, folded from the outcomes magus
keeps in the run history (history_path). An outcome is recorded when a job that
ran at a tier ends: its holder ends it, the sweep or magus job prune ends it as
no_return, or the session holding it closes.
It runs once, at fork, with the job store read-only: every magus\job member
raises inside it. It returns null to let the default decide, or an object with
tier, skills and a non-empty reason. A result missing any of the three fails
the fork.
import "magus";
import "magus/modeltiers" as modeltiers;
object Choice { tier: modeltiers\ModelTier, skills: magus\SkillVariant, reason: str }
fun economyHolds(req: ModelTierRequest) > bool {
foreach (r in req.history) {
if (r.rank == 0 and r.kind == req.kind and r.jobs >= 10 and r.passLower < 0.5) { return false; }
}
return true;
}
fun choose(req: ModelTierRequest) > Choice? {
if (req.readOnly or req.check == null) { return null; }
if (req.writeFiles <= 3 and req.writeProjects.len() == 1 and economyHolds(req)) {
return Choice{
tier = modeltiers\ModelTier.economy,
skills = magus\SkillVariant.full,
reason = "{req.kind.value} job, {req.writeFiles} files",
};
}
return Choice{
tier = modeltiers\ModelTier.standard,
skills = magus\SkillVariant.short,
reason = "wide write set, or economy failing {req.kind.value}",
};
}
magus\job.modelTier(choose);
This one sends small, checked jobs to economy until economy has failed half of
ten or more jobs of the same kind. The history is the same set of numbers
magus insight reports and the model-tier-fit check in magus doctor judges,
so the matrix and a person read one record.
The same file is the compiled example under std/examples/magus.
Beside passLower over every ending, firstPassLower bounds the pass rate of first
attempts alone (first_pass_lower in magus insight), so a tier that passes only
once a job is forked again reads lower there.
Read-only and unchecked jobs fall through to the default, because a job nothing can verify is the wrong place to try a cheaper model.
Where to go next
- Jobs: the plan a tier is recorded on.
magus\job.modelTiersandmagus\job.modelTierin the Buzz reference.