A cheap model in one channel, a frontier model in another: how channel_overrides works (and the cache bug that made it expensive)

You run one Hermes bot in your Discord server. In #daily people just chat and look things up, so paying frontier-model prices there is pure waste. In #dev people paste code and want a review, and a cheap small model gives you confident nonsense. You do not want two bots, two configs and two memories. You want the same bot to behave differently depending on where it is talking.
Hermes has supported this for a while — the setting is called channel_overrides. But until this week it came with a cost that was genuinely hard to notice: whenever a chat’s model came from that override, Hermes threw away its cached agent after every single turn and rebuilt everything on the next one. The chat kept working. It just felt sluggish, and it quietly cost more.
PR #133948, merged October 6, fixed it. Here is the feature first, then the trap.
Why you would want per-channel models
A single gateway can serve Telegram, Discord, Slack and others at once, and each platform has many channels, groups and threads. By default every chat on a platform shares one model and one system prompt.
That breaks down in two familiar situations:
- Cost. A chat channel does not need a frontier model. A few hundred messages a day at flagship prices adds up fast.
- Persona. A code-review channel needs a reviewer’s voice; a general channel needs a light assistant. One shared global prompt is bad at both.
channel_overrides exists for exactly this: within one gateway, give specific channels or threads their own model, provider and system_prompt.
Setting it up
The config lives in ~/.hermes/config.yaml, under the platform. Say your #dev channel id is 123456789012345678 and #daily is 987654321098765432:
platforms:
discord:
enabled: true
channel_overrides:
"123456789012345678": # channel or thread id
model: anthropic/claude-sonnet-4.6
provider: anthropic
system_prompt: "You are the #dev channel code-review specialist."
"987654321098765432":
model: openai/gpt-5-mini
All three keys are optional. Set only model, only system_prompt, or any combination — anything you leave out falls back to the global default. The system_prompt override is ephemeral: it is injected per turn and never stored in history, so it does not pollute your conversation record.
Restart the gateway (or reload config from a session), then send a message in an affected channel to confirm.
Priority: a manual switch in the chat always wins
The resolution order for the model is:
- the session’s own
/modeloverride — what a user picked inside that chat; channel_overrides— the per-channel default you configured;- global config / environment.
So a channel override outranks the global default, but never beats a user who deliberately ran /model in that conversation. Lookup also goes exact channel or thread id first, then the parent channel id — which means Discord threads inherit their parent channel’s override automatically. You do not need one entry per thread.
The trap: an overridden chat was wiped every turn
To save money, the gateway caches a per-session agent in memory so consecutive messages reuse the same prepared runtime — tools, context pins, and everything else that is expensive to build. While that cache is warm, a run of messages is fast.
The problem was in the post-turn self-check. It decided whether the turn had fallen back to another model by comparing the agent’s model against the global model. But a chat with channel_overrides is supposed to differ from the global model. So the check concluded, on every single turn, that a fallback had just happened — and dropped the cached agent along with its session context pin.
The visible result: every turn in that channel rebuilt the agent from scratch. No crash, no loud error in the logs, just “this bot feels a bit slow” and a bigger bill than expected.
Why this deserved a P0
Rebuilding an agent is not just a few milliseconds. It also invalidates the upstream prompt cache, whose key includes the model and the context prefix. Once the prefix changes, the next request has to read the entire conversation again at full input price. In a chat with dozens of turns, that means paying full freight for the same history over and over.
That is why the fix is labelled P0. It does not break anything loudly — it wastes money continuously, silently, every day, and it targets precisely the users who configured things correctly. The more you tried to save, the more you were charged.
What the fix does
PR #133948 rebuilds that baseline the same way the agent itself was built:
- the baseline resolves in the same order — the session
/modeloverride actually used for the turn, then the channel override, then the global model; - it normalises for the agent’s primary provider and pins on the primary route, so a real fallback onto another provider or onto the welcome host is still detected;
- before comparing names at all, it checks whether the agent has an active provider-fallback flag. Only a genuine fallback activation sets that flag, and a model-name comparison alone cannot see a fallback that serves the configured model from a different endpoint.
It also fixed the opposite error: turns that had really fallen back used to be treated as healthy and kept their cache. Both directions of misclassification are narrower now.
Checking your own setup
If you use channel-level model overrides:
- open
~/.hermes/config.yamland confirm the models underplatforms.<platform>.channel_overridesare what you expect; - send a message in the affected channel and watch the time to first token; before the fix it stayed noticeably higher there than elsewhere;
- if your gateway serves several profiles, note this is a different layer from the model overrides config:
model_overridespicks models per task inside one profile,channel_overridespicks the model per platform channel. They stack.
Caveats worth knowing
- Manually switching models clears the prompt cache. Changing the model with
/modelmid-session changes the cache key, so the next message re-reads everything at full price. That is by design, not a bug. To make it stick, use/model --globalto write it intoconfig.yaml— but if that chat has achannel_overridesentry, the channel setting deliberately keeps winning, otherwise a one-off global switch would silently erase your per-channel design. - Provider-only overrides (no
model) still compare against the global model. That is a known edge case, unchanged from before. - Do not “fix” a cache problem by disabling the agent cache. The cache is the point; the bug was in the comparison logic and it is gone. Update Hermes to a build that includes the fix.
The short version
channel_overrides is an underrated feature: one gateway, one memory, one config, and each channel gets the right model and persona — with the expensive model reserved for where it earns its keep. It also produced a textbook silent-cost bug: nothing failed, you just paid more. Get the config right, and run a Hermes build that includes the fix.
If caching economics interest you, prompt-cache sharing across profiles is the next layer of the same story.