Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

TierRolesModel
frontiercoding, review, designThe provider's default large model
workhorseresearch, summarizeIts default small model
fastquick, commit-message, title, progress-checkIts 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 models and roles.

  • 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:

  1. the profile default_profile names, at the top of the file:

    default_profile = "work"
    
  2. else the profile called default;

  3. 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

FlagEffect
--profile NAMERun on this profile
--tier NAME=MODELRun one tier on another model: a tier name, provider/model-id, or a model id one configured provider offers. Repeatable
--model MODELShort 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

KeyMeaningDefault
modelsprovider/model-id references, in order. All must name the same model; several entries mean several providers for it.Required for a new tier
rolesThe roles this tier fills, as a list.Required for a new tier
max_tokensOutput 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"
effortWith 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

RoleUsed by
codingAn ordinary turn
review--review turns
design--plan and --design turns
quick--quick turns
researchNothing yet (subtasks)
summarizeWork summaries and context state updates
commit-message/commit's drafted message
titleNaming /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.