Tiers and Profiles
A tier names one model, says which providers to reach it through, and which roles it fills. A profile is a named set of tiers, and every conversation runs on one profile: it decides where all of the conversation's work goes, its turns and its commit messages and titles alike.
Built-in profiles
Every configured provider the catalog has defaults for brings a profile named by its id, with three tiers:
| Tier | Roles | Model |
|---|---|---|
frontier | coding, review, design | The provider's default large model |
workhorse | research, summarize | Its default small model |
fast | quick, commit-message, title, progress-check | Its default small model |
heph models marks each provider's default large and default small models.
heph profiles lists every profile with its tiers, the default first:
$ heph profiles
Default profile: anthropic
anthropic (anthropic's catalog defaults):
frontier coding, review, design anthropic/claude-sonnet-4-6 thinking high
workhorse research, summarize anthropic/claude-haiku-4-5-20251001 thinking off
fast quick, commit-message, title, progress-check anthropic/claude-haiku-4-5-20251001 thinking off
Progress checks use this conversation's progress-check tier. Older custom
layouts without that role use their quick tier; if neither exists, a repeated
call checkpoint asks the user. Checks always disable thinking and cap output at
1024 tokens. See Permissions.
Experimental native progress
An explicit, separate decide tier can try Ollama native progress before the
existing classifier. Configure any model Ollama can run, including custom names,
aliases, and tags. Heph has no model allowlist and does not require advertised
decision capability, a particular server version, or local weights.
Decision-only models are not probed at startup; Ollama receives the configured
name at runtime and determines whether it supports /v1/systemone and whether
the request fits. Requests retain shared 64 KiB text-body bounds and typed
answer validation, not model-specific prompt caps. API failures are shown as
status messages before fallback. No decider is added to a bootstrap profile or
pulled automatically. Live accuracy and latency validation remain unfinished.
[tiers.decide]
models = ["ollama/nimble:latest"]
roles = ["decide"]
timeout_ms = 1500
[decisions]
progress = "on"
high_probability_threshold = 0.85
For Clef Flash, set models = ["ollama/clef-flash:latest"]; a custom model
might use models = ["ollama/team/my-decider:q8"]. Neither name needs
registration in heph.
The positive timeout includes admission and native failover. Uncertain or failed
native checks fall back once to the existing ten-second classifier. Conclusive
native loops ask the human without another classifier; hard checkpoints and tool
approval remain unchanged. progress = "off" keeps the checkpoint but asks the
human without classification. With no decision tier, today's classifier is
unchanged.
heph decide --schema progress --state FILE audits a bounded schema-state JSON
fixture in its own diagnostic conversation and prints a proposed resolution. It
never applies that resolution or invokes legacy fallback. Permission triage,
turn-end classification, cloud-native decision adapters, and report/replay are
not built. Choice-question chat emulation is available for the opt-in command
output workflow below; configuring a decide tier alone does not enable it.
Successful command output
To keep routine successful build/test chatter out of the working model's context, enable this and restart:
[decisions]
command-output = "on" # defaults to off
command-output-commands = [["make", "check"]]
Ordinary direct cargo build, check, test, and clippy status commands
have built-in eligibility rules. Exact program/argument arrays add project
contracts; there are no wildcards or argument joins. This list is replaced by
the project value, including an empty array. Permissions still control whether
the command can run. Shell/background output is never compacted this way.
Enabling this can send complete eligible command logs and bounded task/intent
evidence to configured native decision recipients, or the selected profile's
quick route if no native candidate resolves before dispatch. It does not
choose a new provider by price. Keep all these routes local if logs must remain
local. Known environment-held credentials veto classification, but unknown
secrets can still be transmitted. Native failure never starts a chat-model
stage.
Exit zero and complete capture are prerequisites, not model judgments. A checked
native or emulated probability of at least high_probability_threshold (default
0.85) for status_only selects a fixed command-success message. It does not
assert that tests ran or all tests passed. Diagnostics, requested data,
uncertainty, provider failure, and oversized evidence retain capped output. Logs
of 400 bytes or fewer skip classification. The shared deadline is the native
tier timeout, or 1,500 ms without a tier. Emulation caps output at 1024 tokens
or the smaller quick-tier ceiling. No savings measurement is claimed, and live
native compatibility is still experimental.
When inspecting output, ask the agent to set exec keep_output = true; this
prevents capture/classification for that call. Help/query modes, test listing,
compile-only tests, requested machine-readable output, and unknown argument
modes retain ordinary output too. No chunked clearance is built.
Terse results include a read_output log ID. The tool selects stdout or
stderr, a 1-based byte offset, and a positive byte limit capped at 16 KiB.
Text rendering replaces invalid or split UTF-8; encoding = "hex" recovers
exact original bytes. Streams are stored separately, not with a total ordering.
Logs survive restart/adoption, are restricted to this conversation's ancestry,
and are deleted with their tool-call evidence. Recovery never reruns a command.
Your own profiles
A [profiles.<name>] builds on another profile and changes only what it writes:
[profiles.work]
extends = "bedrock" # a built-in profile, or another of yours
[profiles.work.tiers.frontier] # replaces this tier's models; keeps its roles and thinking
models = ["bedrock-openai/global.openai.gpt-5.6-sol"]
[profiles.work.tiers.fast]
thinking = "off"
max_tokens = 2048
-
With no
extends, a profile builds on the default profile. -
Each field a tier writes replaces the base tier's; the fields it leaves out, and the tiers it does not mention, are kept.
-
A tier the base does not have is new, and needs both
modelsandroles. -
Two tiers of one profile may not do the same role: the profile is refused with
role title is claimed by tiers fast and extra. -
A profile may not take a provider's name, and profiles may not extend each other in a loop.
The default profile
A conversation that names no profile runs on the default one:
-
the profile
default_profilenames, at the top of the file:default_profile = "work" -
else the profile called
default; -
else the only configured provider's built-in profile, or the only one the catalog has defaults for.
With several providers and none of these, there is no default, and heph says so
when work needs a model: pass --profile, or set default_profile.
The default profile itself, with no extends, builds on the built-in profile of
rule 3. So with one provider, a [profiles.default.tiers.fast] changes the
fast tier and keeps the others:
[profiles.default.tiers.fast]
models = ["anthropic/claude-sonnet-4-6"]
[tiers]
The [tiers] table is the profile default, written whole: it extends nothing,
so it must cover every role it needs. It is how heph was configured before
profiles and still works; writing both [tiers] and [profiles.default] is an
error.
[tiers.frontier]
models = ["bedrock/us.anthropic.claude-opus-5", "anthropic/claude-opus-5"]
roles = ["coding", "review", "design"]
[tiers.workhorse]
models = ["anthropic/claude-sonnet-4-6"]
roles = ["research", "summarize"]
[tiers.fast]
models = ["anthropic/claude-haiku-4-5-20251001"]
roles = ["quick", "commit-message", "title", "progress-check"]
Choosing for a conversation
| Flag | Effect |
|---|---|
--profile NAME | Run on this profile |
--tier NAME=MODEL | Run one tier on another model: a tier name, provider/model-id, or a model id one configured provider offers. Repeatable |
--model MODEL | Short for --tier on the tier doing the conversation's role (frontier for an ordinary turn). Other work keeps its own tiers |
heph --profile work
heph --tier fast=claude-sonnet-4-6
heph --model us.anthropic.claude-opus-5-5 # this conversation's turns only
With no routing, an explicit --model ollama/NAME creates an in-memory local
tier for all roles, including progress-check, and retains that bootstrap with
the conversation. It does not write a config file or replace an invalid profile.
Exactly one provider with catalog defaults supplies existing routing, as do
default_profile, [tiers], and [profiles.default]. Multiple catalog
defaults and valid named profiles without a selected default leave routing
absent, so the explicit Ollama selection bootstraps rather than failing with an
ambiguous-profile error. Configured profiles are validated first and keep their
original layouts and inheritance. An explicit --profile still retains that
profile's auxiliary assignments. Ollama has no automatically selected default
installed model.
With an existing profile, --model changes one tier. A commit message still
goes to the profile's commit-message tier, even when that is on another
provider than the model you named: the profile is where you decide which
provider sees what.
A conversation keeps its profile and overrides. heph --continue NAME routes
the same way it did; flags given with it change what was kept and are kept in
turn, and a new --profile drops the --tier changes made for the old one.
/new and /fork start with the session's routing, and /switch to a
conversation that has none gives it the session's.
heph tiers shows the tiers a set of flags gives, with overrides marked:
$ heph tiers --profile anthropic --tier fast=claude-sonnet-4-6
Profile anthropic (anthropic's catalog defaults; the default):
frontier coding, review, design anthropic/claude-sonnet-4-6 thinking high
workhorse research, summarize anthropic/claude-haiku-4-5-20251001 thinking off
fast quick, commit-message, title anthropic/claude-sonnet-4-6 thinking off (overridden)
In a session, /tiers opens a window over the conversation with the same
profile: which tier the turns run on (marked >), each tier's roles, then its
model and thinking on the line below, and the other profiles you could start
with. Up and Down scroll it; any other key closes it.
Tier keys
| Key | Meaning | Default |
|---|---|---|
models | provider/model-id references, in order. All must name the same model; several entries mean several providers for it. | Required for a new tier |
roles | The roles this tier fills, as a list. | Required for a new tier |
max_tokens | Output ceiling per request, thinking included. | The model's catalog default_max_tokens |
thinking | "adaptive" or "off". | "adaptive" if any role is coding, review, or design, else "off" |
effort | With thinking on: low, medium, high, xhigh, or max. | high |
Thinking is also off for a cloud model the catalog says cannot reason. For Ollama, omission uses the model default; explicit thinking or effort is checked against discovered or overridden controls, not inferred from the name.
A tier's providers must all speak one protocol: Claude's Messages API or GPT's Responses API, or Ollama's native API (Providers). A tier that mixes them is refused when the config loads, with the two protocols named.
Roles
| Role | Used by |
|---|---|
coding | An ordinary turn |
review | --review turns |
design | --plan and --design turns |
quick | --quick turns |
research | Nothing yet (subtasks) |
summarize | Work summaries and context state updates |
commit-message | /commit's drafted message |
title | Naming /fork and /new conversations |
Work that needs a role no tier of the profile has fails with
no tier has role coding; add it to a tier's role in config.
Several providers, one model
A tier with two entries is one model reachable two ways. Today heph uses the
first entry whose provider is configured; automatic failover to the next on a
failed request has not landed. Two entries naming different models are an error
(a tier is one model on several providers): a tier is a price and quality
point, not a list of alternatives. See Models and Cost.