Agent engine config
Configure the session agent's model, gateway, prompt, reasoning effort, and sampling parameters with agent_engine_config, and assign models to the roles installed plugins declare.
The agent_engine_config object in settings.json
configures the engine:
defaultThe one role the core loop has: stella chat, the Command Deck, and a plain
stella run. default_model sets its model. This is the only model setting
stella ships with.
seatsA model for a participant an installed plugin declares. Keyed as
<plugin-id>/<role>. Unset by default, so a plugin runs on one model until
you say otherwise. See Seats.
Every field is optional. It merges across the same user, org-managed, and
project scope chain as the rest of settings.json, field by field. A project
can pin the model while your user scope keeps everything else.
An unrecognized key loads normally and prints a line naming what to write
instead — nothing breaks, and nothing is silently ignored either. Set
default_model for the session's own model, and a seat for anything
a plugin runs.
Full schema
{
"agent_engine_config": {
// The session's model ("provider/slug", or a bare catalog slug).
"default_model": "zai/glm-5.2",
// A model for a role an installed plugin declared. Keyed
// "<plugin-id>/<role>". A seat with no entry runs on the session's model.
"seat_models": {
"vera/verifier": "openrouter/openai/gpt-5.5"
},
// The model vocabulary the TUI pickers offer, and the ceiling on seat
// assignments.
"allowed_models": ["anthropic/claude-fable-5", "zai/glm-5.2",
"openrouter/openai/gpt-5.5"],
// "on" = let stella choose rather than a pin. It does not select a
// model (see Auto modes); it marks whether a /profile holds the dials.
"auto_mode": "off",
// "on" = reasoning effort chosen for you, overriding "effort" below.
"effort_auto": "off",
// "on" = thinking mode chosen for you, overriding "reasoning" below.
"reasoning_auto": "off",
// "on" = the minimal base system prompt: a bare tool advertisement, so
// your own prompt fields carry the prose. `--minimal` for one invocation.
"minimal_prompt": "off",
// Seconds of provider silence that end one generation. The partner of
// params.max_tokens below: raise the output cap without raising this and
// long steps stop on the timeout instead of the cap.
"model_timeout_secs": 816,
// The agent's deep config. Set a field and it goes on the wire; leave it
// out and the provider default applies.
"agents": {
"default": {
"provider": "openrouter", // gateway: the slug goes to THIS
"model": "openai/gpt-5.5", // provider verbatim
"prompt": "You are a terse, test-first engineer.",
"effort": "high", // low | medium | high | xhigh | max
"reasoning": "on", // thinking mode on/off
"params": {
"temperature": 0.2, "top_p": 0.9, "top_k": 40,
"frequency_penalty": 0.0, "presence_penalty": 0.0,
"repetition_penalty": 1.0, "max_tokens": 4096, "seed": 7,
"verbosity": "low", // low | medium | high
"service_tier": "priority" // auto | default | flex | priority
}
}
}
}
}Seats
A plugin describes a process. That process can have several participants: a planner, a reviewer, a second opinion. The plugin never asks for a model. It declares the roles it needs, and you decide which ones are worth a model of their own.
{
"agent_engine_config": {
"default_model": "zai/glm-5.2",
"seat_models": {
"vera/verifier": "anthropic/claude-fable-5"
}
}
}Or in stella.toml, where it is a section of its own:
[seats]
"vera/verifier" = "anthropic/claude-fable-5"- A seat with no entry runs on the session's model. If a plugin has five participants, installing it costs the same as a single-model session, until you add a line here. stella never picks a second model for you on its own — that would be a guess about a role it doesn't understand.
- The key is always
<plugin-id>/<role>. The plugin declares only the plain role name, and stella adds the prefix. This means one plugin can't take over a setting you made for another. Areviewerrole you configured for one plugin is never inherited by a different plugin that also declares areviewerrole. allowed_modelssets the ceiling here too. If a seat names a model outside a non-empty list, stella refuses it, shows a notice, and falls back to the session's model.
Quote the key in TOML. vera.verifier is dotted-key syntax and would parse as
a table vera containing verifier.
Model precedence
The session's model. First hit wins:
- The
--modelCLI flag (orSTELLA_MODEL). This suppresses the settings below for this invocation. agents.default.model, sent verbatim toagents.default.providerwhen that is setdefault_model- Auto-detection / the provider's own default
A plugin's seat. First hit wins:
seat_models["<plugin-id>/<role>"]- No entry: the seat runs on the session's model resolved above.
When --model suppresses a configured model, the run prints a note on
stderr — engine config: model ... skipped — --model pinned ... for this invocation — so a dropped setting is never silent. Settings control the
model whenever the flag is absent.
Every override here is soft. If a configured model's provider has no credential, or its adapter fails to build, stella skips it with a notice on stderr. It then falls back to the session's model, exactly as it would with no config at all. The notice names what was skipped and what runs instead:
! seat `vera/verifier`: no credential resolved for provider `openrouter` —
this seat runs on the session's model.stella config prints the resolved provider, session-default model,
credential source, base URL, and dialect. These are the inputs those chains
start from. stella models list --all shows which slugs each provider will
accept:
stella config
stella models list --allOne gateway per role
The agent's provider field sends its model slug straight to that provider,
with no prefix splitting. Seat assignments take a full provider/slug string.
Together, this lets you mix billing relationships — your own key for each
provider — in one run:
{
"agent_engine_config": {
"default_model": "anthropic/claude-fable-5", // the session — your Anthropic key
"seat_models": {
"vera/verifier": "openrouter/openai/gpt-5.5" // your OpenRouter key
}
}
}Two keys, two bills, one run:
export ANTHROPIC_API_KEY=sk-ant-...
export OPENROUTER_API_KEY=sk-or-...
stella run --pipeline vera "add a per-provider breaker cooldown"Without a provider field, model strings follow --model rules:
provider/slug splits on the first / when the prefix names a known
provider. So openrouter/openai/gpt-5.5 routes openai/gpt-5.5 through
OpenRouter. A bare slug resolves through the model catalog instead.
Auto modes
Set any of these to "on" and stop thinking about it:
auto_modeIt does not select a model. What it marks is whether a
/profile holds the dials or stella does.
Default off
effort_autoReasoning effort chosen for you, overriding agents.default.effort.
Default off
reasoning_autoThinking mode chosen for you, overriding agents.default.reasoning.
Default off
minimal_promptRun every session on the minimal base system prompt. This is a bare tool
list in place of the built-in persona, so the model is steered by what you
configure instead: agents.default.prompt (which adds to the minimal
base rather than replacing it), workspace memories, rules, and
SessionStart hook context. --minimal (or STELLA_MINIMAL=1) forces it
on for one run. The flag can never turn off a mode you configured. In
stella.toml this is [agents] minimal_prompt, and the interactive
session's SETTINGS → AGENTS pane shows it as a GLOBAL toggle.
Default off
headless_scope_bypassSetting this does nothing. No code reads this value, at any setting.
The key still loads and merges instead of being rejected as unknown. This is on purpose: it's part of a published benchmark setup, and removing the key would break scores that were already reported. If you have this set, delete it — nothing is waiting on you to use it.
Default off
model_timeout_secsSeconds of silence from the provider that end a single generation. This
limits the gap between stream fragments, not the total time, so a step
that keeps streaming is never cut off. 0 removes this limit entirely.
Set it alongside params.max_tokens, not instead of it. The two work as
one budget. If you allow 128,000 output tokens but size the timeout for
64,000, a step can hit the timeout first. That looks like a model that
couldn't finish, when really the timeout just wasn't scaled to match.
Default 816
When an auto is "on", it overrides the matching per-agent setting. That's
the rule.
The smallest useful config is one line:
{
"agent_engine_config": {
"default_model": "zai/glm-5.2"
}
}Add a second model only where an installed plugin has a participant worth running on one:
{
"agent_engine_config": {
"default_model": "zai/glm-5.2",
"seat_models": { "vera/verifier": "anthropic/claude-fable-5" }
}
}Output token ceilings
By default, every model is asked for its own maximum output. stella reads
that ceiling from the model catalog, which learns it from the provider's own
/models endpoint. You never have to configure anything to get a full-length
answer. A cap set below the model's ceiling decides where work stops instead
of the model deciding, and a step cut off in the middle of reasoning sends no
tool call and does no work at all.
So the only thing worth configuring here is asking for less than the model allows. You might want to bound cost on a long unattended run, cut latency, or match a comparator that stops lower.
--max-output-tokens
stella run "audit the auth module" --max-output-tokens 32000This applies for one run only. It's a global flag that applies to every call
in that run, also settable as STELLA_MAX_OUTPUT_TOKENS. Use it when you want
one run to behave differently, or when you're debugging a configured value —
it outranks both settings below for exactly that reason.
[models.output_caps]
[models.output_caps]
"anthropic/claude-sonnet-5" = 64000 # this model on this provider
"deepseek-chat" = 32000 # this model on any routeThis applies for the current workspace. Keys are provider/slug or a bare
slug. The provider-qualified form wins when both match, so you can cap a
model only on the gateway you pay per token for. Entries merge per model
across settings scopes, so a project pinning one model never drops your pins
on others.
This only overrides the default; it never defines a new one. A model that's missing from the table just gets its own maximum, so a model that ships tomorrow needs no entry. Deleting an entry restores the correct default instead of leaving a stale number behind.
Precedence
Highest wins:
--max-output-tokens(this invocation)[models.output_caps](per model)agents.default.params.max_tokensunderagent_engine_config- the model's own ceiling, from the catalog (the default)
A value above the model's real ceiling is clamped, not sent. Asking a provider for more output than the model can write doesn't get you a longer answer. It gets a rejection on every request, which still bills the round trip and then needs a retry. Clamping gives you what an oversized number was asking for anyway.
0 is refused, not read as "unlimited." Leaving the field empty already
means "the model's own maximum," so 0 has no second meaning left to carry.
This is different from model_timeout_secs, where 0 does mean "no limit."
Where ceilings come from
| provider | field read from its /models |
|---|---|
| Anthropic | max_tokens (output) and max_input_tokens (context) |
| OpenRouter | top_provider.max_completion_tokens |
| Gemini | outputTokenLimit |
| OpenAI-compatible | max_output_tokens or max_completion_tokens |
stella deliberately does not read a bare max_tokens field on an
OpenAI-compatible listing. On some gateways it means the output cap; on
others it means the whole context window. Reading it the wrong way would set
a ceiling many times larger than the model's real one.
Bedrock and Vertex don't publish a listing stella can read, so those rows
carry a seeded value instead. stella models refresh (and the startup sync)
replaces seeded numbers with the provider's own numbers wherever one is
published.
One model's number is an estimate: grok-4. No catalog source publishes
a cap for that exact slug. Its close relatives — grok-4.3 and the
grok-4.20-* variants — all report 30,000, so that's what ships. The newer
grok-4.5 reports 500,000, but using a different generation's number could
cause a rejection on every request, instead of just leaving some headroom
unused. This gets corrected automatically the first time xAI's listing
publishes a real number.
Generation parameters
Every entry under params follows one rule: if you don't set a field, it's
left out and the provider's own default applies. This keeps the request body
identical to having no config at all, which keeps prompt caching intact. If
you do set a field, it's sent on the wire.
Each provider adapter forwards only the parameters its wire format supports, and maps reasoning to that provider's own mechanism:
GLM's own request-level thinking object. Also forwards top_p, top_k,
frequency_penalty, presence_penalty, repetition_penalty, and seed.
The normalized reasoning object. Some upstream models behind OpenRouter
make reasoning mandatory and reject reasoning: {enabled: false} with a
hard 400 error. When that happens, stella resends the request once
without the object, instead of losing the call — turning reasoning "off"
is a cost preference, not a requirement for correct output.
Sent only for Grok models that accept this parameter. grok-4 answers it
with a 400 error, so this is limited by model, not just by provider. The
grok-4.3 default accepts it.
Models from Claude 4.6 onward, including the 5 family, take
thinking: {"type": "adaptive"} plus output_config.effort, and reject
budget_tokens and the sampling parameters. Models 4.5 and earlier use
{"type": "enabled", "budget_tokens": N} with a budget based on effort
tier, and do accept sampling — though temperature is left out whenever
thinking is on, because the Messages API rejects any value but 1 alongside
it.
Plus text.verbosity and service_tier from params. temperature and
top_p are left out for reasoning models — an API rule, not a choice
stella makes.
Gemini 3 can't fully turn off thinking, so "off" maps to the lowest
level the model documents — as close to "no thinking" as the model
allows. verbosity, service_tier, and repetition_penalty are dropped.
There's no reasoning field that works across every Chat Completions server, and an unknown key risks a hard 400 error on a server you didn't choose to experiment with — so these providers send neither shape. Sampling parameters still go through.
If a provider can't accept a parameter, stella drops it silently. An unsupported setting never fails a request.
Custom prompts
agents.default.prompt replaces the built-in base instructions for the
session agent. Workspace memories and rules still get added on top — they're
workspace context, not part of the base persona.
A plugin's participants are the plugin's own: it ships their instructions, and
agent_engine_config decides only which model each one runs on.
Config panel
Everything above is editable in the Command Deck. The editor is the
full-width body of the SETTINGS tab, the home for all config. Open it with
/settings or by tabbing to it:
- Press
eon the SETTINGS tab to focus the panel. It has a GLOBAL tab (auto modes,allowed_models) and the agent's own tab, in the orderGLOBAL default. Navigate with ↑/↓. Press ⏎ to edit a row: enum rows cycle, and numeric or text rows take inline input. Pressxto clear a field back to "provider default." Press Esc to hand the keyboard back to the tab. - Press ⏎ on the model row to open a type-to-filter model picker. It lists
your
allowed_models, or the full catalog when that list is empty. The SETTINGS tab is the one place you configure models — there are no per-agent slash commands. - Press
sto save to your user settings (~/.stella/settings.json). PressSto save to the project (<workspace>/.stella/settings.json). Saves keep every other key in the file untouched, and apply to runs started from then on.
Degradation is always soft
Configuration can never turn a runnable session into an error. If a configured model's provider has no credential, or its adapter fails to build, stella skips it with a visible notice. It then falls back to the session's model, exactly as it would with no config at all.
The router that resolves each seat follows the same rule while a run is in progress:
- Per-provider circuit breakers. Three transport failures in a row open a provider's breaker. Routing then skips that provider and reports the fallback visibly — it never switches silently in the middle of a session. After a cooldown, one trial call decides whether the breaker closes again. See Troubleshooting for what an open breaker looks like.
- Soft degradation everywhere. A configured seat model whose provider has no credential falls back to the session's model with a notice.
agent_engine_config carries no credential routing of its own. An agent's
provider field references provider ids, and their base_url and
api_key_env stay protected by the project-scope
trust boundary. A cloned repo can suggest
models, but it can't redirect your keys.