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 /goal satisfaction 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: auto means “inherit the main model” — it is not a real provider name. To pin a specific cheap model, set both provider and model.
  • The timeouts are not arbitrary: approval classification defaults to 30 s (it must be quick), kanban_decomposer gets 180 s, curator 600 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_review has an extra max_input_tokens: 600000 capping 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: auto means “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 for compression when 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.