The Second Model You Never Configured: Hermes' Auxiliary Call Lane

One Monday morning you open your session list and yesterday’s important conversation is titled {"title": "Fix the CI... — a truncated JSON fragment. Another time you switch the main model to a cheap fast one and command approval goes strange: harmless commands get blocked, obviously dangerous ones sail through. Someone else running llama.cpp locally complains that “one compression takes several minutes”. These three symptoms look unrelated, but their root is often the same invisible lane: auxiliary tasks.
The call lane you use every day and have never configured
Your main model is not doing everything itself. Besides the conversation you see, Hermes fires a stream of small background requests:
- naming each new session;
- summarising when the context window fills up;
- classifying whether a command should stop and ask you;
- asking a vision model to describe an image when your main model is text-only;
- skills search, memory query rewriting, kanban task decomposition, the
/goalsatisfaction judge, mail scoring, and the end-of-turn self-review that saves memory or patches a skill.
These tasks have names in the code. As of main there are 13 built-ins:
| Task key | Display name | What it does |
|---|---|---|
vision |
Vision | image/screenshot analysis (the describer for text-only mains) |
compression |
Compression | context summarisation |
approval |
Approval | smart command-approval classifier |
mcp |
MCP | MCP tool reasoning |
title_generation |
Title generation | session titles |
review |
Review | the /review reviewer subagent |
memory_query_rewrite |
Memory query rewrite | rewrites memory retrieval queries |
tts_audio_tags |
TTS audio tags | Gemini TTS tag insertion |
skills_hub |
Skills hub | skills search and install |
triage_specifier |
Triage specifier | expands a kanban Triage note into a spec |
kanban_decomposer |
Kanban decomposer | decomposes a task into a child-task graph |
profile_describer |
Profile describer | auto profile descriptions |
curator |
Curator | skill-usage review pass |
There are also goal_judge (/goal satisfaction), monitor (important-mail scoring), background_review (the post-turn self-improvement fork) and the two MoA slots moa_reference / moa_aggregator. They inherit your main model by default, which is why you may have run “one model doing everything” for months without noticing this lane exists — until it breaks.
The config shape: one key decides which model a task runs on
Every task takes the same block shape:
auxiliary:
# global knobs
transient_retries: 2 # same-provider retries for a transient blip, clamped 0..6
free_only: false # true = background traffic never uses a PAID OpenRouter lane
openrouter_model: "" # override the auto-chain's OpenRouter model
stream_only_base_urls: [] # endpoints that reject non-streaming chat (HTTP 400)
# one block per task, same shape
title_generation:
enabled: true # false = no titles at all
model_upgrade_enabled: true # false = keep the instant derived title, never call a model
provider: auto # auto = inherit the main model
model: "" # empty = follow the provider
prefer_fast_model: false # true = use the provider's fast tier
language: "" # language for titles
timeout: 30
approval:
provider: auto
model: "" # a fast, cheap model is recommended here
timeout: 30
curator:
timeout: 600 # skill review can run minutes on a reasoning model
Three easy mistakes:
provider: automeans “inherit the main model” — it is not a real provider name. To pin a specific cheap model, set bothproviderandmodel.- The timeouts are not arbitrary: approval classification defaults to 30 s (it must be quick),
kanban_decomposergets 180 s,curator600 s. Compressing with a local model is the classic case where 120 s is not enough — the usual source of “compression is so slow locally”. background_reviewhas an extramax_input_tokens: 600000capping the total input the self-review replays. Route it to a different model and it replays a compact digest instead of the full conversation, roughly 3–5× cheaper.
The easiest place to change this is the bottom entry of the model menu:
hermes model # pick "Configure auxiliary models..." in the menu
hermes config get auxiliary.title_generation # see the effective values
Plugins can register their own auxiliary tasks too, and they show up in the same menu. Run hermes config get <key> before you configure anything — these keys are still moving: on this machine’s v0.21.0 CLI, title_generation has no model_upgrade_enabled field, while main already does.
The 17 September batch: ten faces of silent failure
On 17 September 2026 a whole group of fixes for this lane merged. What they share: failure never raises an error, it just produces a strange symptom.
| What you see | The real cause | PR |
|---|---|---|
| A title that is sometimes a JSON fragment | title generation read reasoning_content as prose, or stored truncated JSON |
#113960 |
| Titles never complete, retries pile up | no recovery path after stacked parameter rejections | #113958 |
| Turning the model upgrade off leaves titles empty | the instant derived title should survive, but a model call overwrote it | #113955 |
| One dead endpoint stalls background traffic for ages | auto-titles should give up after one timeout window and name the endpoint that timed out | #113968 |
| After the main model hits its quota, the whole lane stalls | the fallback_providers walk did not skip a quota-limited candidate |
#113965 |
| DeepSeek returns 400 on structured output and fallback dies with it | a 400 must not kill fallback candidates nor cost a doomed first request | #113966 |
| Everything background fails once xAI’s OAuth token expires | auxiliary calls should recover, not fail | #113964 |
| A custom provider returns a silent 401 | key_cmd is a callable, and five resolution branches either called .strip() on it or sent its repr as the bearer token |
#114333 |
A local ollama base_url never takes effect |
that lane never resolved the local address and never actually called the server | #113961 |
/goal judging fails with an opaque judge error |
it should say the Nous authentication failed | #113963 |
The most instructive one: a custom provider that uses key_cmd to mint a token on demand (the only correct approach when credentials expire). The main runtime always passed the returned callable through untouched, while five branches of the auxiliary client assumed a string — so they either crashed, or sent <CommandTokenSource object at 0x…> to the server as the bearer token. The second outcome is worse: it does not error, it just quietly 401s.
Why bugs like these are so hard to track down
Because they live on an “auxiliary” path that is designed to degrade: a failed title must not break your chat, a slow approval classifier should fall through to a prompt, a failed compression needs a safety net. Degrading gracefully is correct design, and the price is that failures get swallowed. You see symptoms — odd titles, unhelpful approvals, slow compression — instead of an error message.
Which leads to one very practical recommendation: pin a fast, cheap model for each auxiliary task (especially approval and title_generation). Then when you switch main models, downgrade one, or lose access to one temporarily, background behaviour stays stable — and while debugging you can tell instantly whether “my main model changed” or “the background lane changed under me”.
Release status
Every fix above merged into main on 17 September 2026. The newest tagged release, v0.21.3 (tag v2026.9.14, 14 September), does not include them. Check what you have:
hermes --version
hermes config get auxiliary.title_generation
If one of these is actively biting you — JSON-fragment titles and the silent 401 on custom providers are the two worst — run hermes update and wait for the next tag, or track main locally in the meantime.
Further reading: to stop auxiliary calls burning money, start with how to configure reasoning effort; frequent compression is the same family of problem, covered in compression and the lean tail; timeouts and port details when a local model runs your auxiliary tasks are in the local llama.cpp guide; and onboarding new providers covers what to watch when you hang auxiliary tasks off a new vendor.
Summary
- Hermes has a background call lane (auxiliary) with 13 built-in tasks plus MoA/self-review slots, all inheriting the main model by default.
- Each task takes the same config block:
provider/model/base_url/api_key/timeout/extra_body/reasoning_effort;provider: automeans “follow the main model”. - To save money and stay predictable: pin cheap models on high-frequency small tasks (
approval,title_generation), and raise the timeout forcompressionwhen a local model handles it. - The 17 September batch fixes ten silent failures (JSON-fragment titles, a stalled quota fallback, the silent 401 on custom providers, and more) and is not in any tagged release yet.