Skip to content

feat: per-channel model and system prompt overrides for gateway platforms (Fixes #1955)#1991

Open
crazywriter1 wants to merge 4 commits into
NousResearch:mainfrom
crazywriter1:feat/1955-channel-overrides
Open

feat: per-channel model and system prompt overrides for gateway platforms (Fixes #1955)#1991
crazywriter1 wants to merge 4 commits into
NousResearch:mainfrom
crazywriter1:feat/1955-channel-overrides

Conversation

@crazywriter1

@crazywriter1 crazywriter1 commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #1955

Problem

The gateway uses a single global model and system prompt for all channels. Different channels (e.g. Discord #daily vs #dev) cannot use different models or personas without running separate bot instances.

Solution

  • Config: channel_overrides per platform — keys are channel/thread IDs; values are optional model, provider, system_prompt.
  • Priority: session /modelchannel_overrides → global default.
  • Lookup: chat_idthread_idparent_id (threads inherit parent channel when no thread-specific entry).
  • Provider: override provider resolves credentials via resolve_runtime_provider; explicit model is kept when both are set.
  • Prompts: channel_overrides.system_prompt in run_sync; existing channel_prompts via adapter unchanged (not duplicated).
  • YAML: platforms.discord.channel_overrides or top-level discord.channel_overrides (same bridge style as channel_prompts).
  • Rebased on latest main. Scope is 4 files only (removed delegate_tool and unrelated test-only diffs).

Changes

  • gateway/config.pyChannelOverride, PlatformConfig.channel_overrides, roundtrip + YAML bridge
  • gateway/run.py_get_channel_override, channel layer in _resolve_session_agent_runtime(user_config), run_sync integration, thread/parent lookup, safe when test mocks lack config.platforms
  • tests/gateway/test_config.py — config roundtrip + top-level YAML bridge test
  • tests/gateway/test_channel_overrides.py — lookup, model/prompt, priority (session > channel > global), parent/thread inheritance

Example config

discord:
  channel_overrides:
    "1234567890":
      model: openrouter/healer-alpha
      provider: openrouter
      system_prompt: "You are a daily news summarizer."
    "9876543210":
      model: anthropic/claude-opus-4.6
      provider: anthropic
      system_prompt: "You are a coding assistant."

Or under platforms:

platforms:
  discord:
    enabled: true
    channel_overrides:
      "1234567890":
        model: openrouter/healer-alpha
        provider: openrouter
        system_prompt: "You are a daily news summarizer."

Testing

pytest tests/gateway/test_config.py tests/gateway/test_channel_overrides.py -q
  • Channel override lookup, model/prompt fallback, session vs channel priority
  • Parent channel model inherited in threads
  • discord.channel_overrides loaded from top-level config.yaml

rivercrab26 pushed a commit to rivercrab26/hermes-agent that referenced this pull request Mar 26, 2026
…eway platforms

Enables different channels (e.g. Discord #daily vs #dev) to use different
models, providers, and system prompts without running separate gateway instances.

Config example:
  platforms:
    discord:
      channel_overrides:
        '1234567890':
          model: openrouter/healer-alpha
          provider: openrouter
          system_prompt: You are a summarizer.

Resolution order: channel_overrides[chat_id] > global config default.

Cherry-picked core functionality from PR NousResearch#1991 (by crazywriter1),
adapted to current main without regressions.

Closes NousResearch#1955
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the contribution — the feature design is solid and the use case is real (different models/personas per Discord channel without multiple bot instances).

Putting this on hold for now. We're working through some foundational gateway changes (config loading, provider resolution, auxiliary client rework) and want those to settle before adding a new config surface area like channel_overrides. Re-adding per-channel complexity on top of things still in flux would make both harder to maintain.

A few notes for when we revisit:

  • The core feature (~120 lines: ChannelOverride dataclass, PlatformConfig extension, resolution helpers) is clean and well-tested
  • The bundled unrelated changes (delegate_tool.py, conftest.py, test_slack.py, test_whatsapp_connect.py) would need to be dropped — those files have diverged significantly on main
  • _resolve_gateway_model() now takes a user_config param on main, so the integration point in run_sync() will need updating
  • Resolution order should be clearly defined: per-session /model override → channel_overrides → global default

Will re-review once the current foundational work stabilizes. The issue (#1955) stays open.

@crazywriter1

Copy link
Copy Markdown
Contributor Author

Thanks for the review. @teknium1 I’ll address the comments and update the PR for re-review.

@PaulBlackSwan

Copy link
Copy Markdown

Drive-by user feedback: this is the exact feature I tried to re-propose in #17834 (closed as dup). My use case: a Telegram bot with 10+ topics where the cost/latency tradeoff varies wildly — research and code-review topics deserve Claude Opus 4.7, daily/personal topics work fine on local Ollama, and switching globally with /model doesn't survive the next thread.

The channel_overrides shape with model + provider + system_prompt is the right level of generality (covers channel_prompts + the model use case in one config block). Tests look thorough. Hoping this can land — happy to test the branch against my deployment if it'd help.

@PaulBlackSwan PaulBlackSwan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by review (not a maintainer, just a future user). Overall the core feature is clean and the dataclass design feels right — model + provider + system_prompt packaged as ChannelOverride is the right level of generality. A few questions and observations:

1. Apparent duplicate _resolve_gateway_model

The diff adds a no-args version at gateway/run.py:~138:

def _resolve_gateway_model() -> str:
    """Read model from env/configmirrors the resolution in _run_agent_sync.

…but the existing _resolve_gateway_model(config: dict | None = None) -> str at the original L432 (now L453) is kept unchanged. Two top-level functions with the same name → the second definition wins, so the new one looks like dead code. Was the no-args version meant to replace the original, or is there a leftover from a rebase? Either way, only one should remain.

2. _build_media_placeholder deletion

The hunk anchored at @@ -313,8 +314,28 @@ shows _build_media_placeholder being partially deleted (its def line + docstring opening are removed). Either:

  • the rest of the function is also deleted but compressed into the diff visualization, or
  • only the head of the function got deleted, which would be a syntax error.

Could you confirm? And if it's a real deletion, this seems unrelated to channel overrides and should probably live in its own commit / PR.

3. Unrelated change in tools/delegate_tool.py

+    child_saved_tool_names = list(model_tools._last_resolved_tool_names)
...
+    child._delegate_saved_tool_names = child_saved_tool_names

Toolset state preservation for delegated agents — clearly unrelated to per-channel overrides. Would be much easier to review (and bisect later) as a separate PR.

4. Test hygiene mixed into a feature PR

The asyncio loop fix in conftest.py, the Slack users_info AsyncMock default, and the warning filters in test_slack.py / test_whatsapp_connect.py are nice cleanups but unrelated to this feature. You mention this explicitly in the description ("Suite hygiene"), but they make the diff bigger to review and would be more useful merged independently — they could land today regardless of the channel-override discussion.

5. Silent skip on malformed channel_overrides entries

for cid, ov_data in raw_overrides.items():
    if isinstance(ov_data, dict):
        channel_overrides[str(cid)] = ChannelOverride.from_dict(ov_data)

A user with a typo like

channel_overrides:
  "1234567890": "openrouter/healer-alpha"   # string instead of object

would have their override silently dropped — gateway boots fine, but the channel routes to global default with zero log signal. A logger.warning("Ignoring channel_overrides[%s]: expected dict, got %s", cid, type(ov_data).__name__) would make this debuggable.

6. Missing parent-channel fallback (parity gap with channel_prompts)

resolve_channel_prompt(config_extra, channel_id, parent_id=None) falls back to parent_id for forum threads / topics that inherit from their parent channel. The new _get_channel_override only takes chat_id, so a Discord forum post inside a channel that has an override won't inherit it.

For Telegram topics specifically (chat with multiple message_thread_id), this matters: the natural mental model is "set override on chat 123, all topics inside it inherit" with optional finer-grained overrides per topic. Without parent fallback, every topic ID has to be enumerated explicitly.

Would suggest mirroring the prompt resolver:

def _get_channel_override(
    config, platform, chat_id, parent_id=None,
) -> Optional[ChannelOverride]:
    ...
    for key in (chat_id, parent_id):
        if not key: continue
        ov = platform_config.channel_overrides.get(str(key))
        if ov: return ov
    return None

7. Tests I'd want to see

The 1215-passing-tests claim is great. I'd add:

  • Provider override path (_resolve_runtime_agent_kwargs_for_provider is brand new but only indirectly tested) — covers the if channel_override and channel_override.provider: branch.
  • Precedence: an integration-ish test where both _session_model_overrides[session_key] and channel_overrides[chat_id] are set — confirms the documented "session > channel > global" order matches the code.
  • Combined combined_ephemeral when channel override has system_prompt AND a context_prompt is present — asserts both are joined with \n\n.

8. Schema docs

If this lands, the discord.md / slack.md / telegram.md user guides need a section parallel to the existing channel_prompts docs. Easy to forget but important for discoverability.


For what it's worth, I have a real production use case hitting this exact need (Telegram with ~10 topics of varying cost/latency requirements), so I'd test the branch end-to-end against my deployment if that helps unblock review.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery labels Apr 30, 2026
@apoapostolov

Copy link
Copy Markdown

@crazywriter1 pretty please with sugar on top can we revive this? 🙏

Real talk — I just ran into this wall hard. I've got Discord channels doing songwriting and deep research that desperately want Claude Sonnet, and other channels running automation/file ops that are perfectly happy on DeepSeek flash. Currently I'm either burning money running Sonnet everywhere or manually /model-ing every session like a caveman.

The PR looks clean, the tests pass, the design is right. What's blocking it from getting over the line? Happy to help with the rebase or any of the fixes PaulBlackSwan flagged if that's what's needed — the unrelated file changes, the param update, the parent-channel fallback. Just want this shipped.

@crazywriter1

Copy link
Copy Markdown
Contributor Author

Thanks everyone for the feedback and detailed review. I haven’t abandoned this PR. I’m planning to update/rebase it soon against the latest gateway changes and address the review points (conflicts, unrelated changes, fallback behavior, tests, etc.). Appreciate the interest and the real-world use cases shared here 🙏

…ousResearch#1955)

- config: ChannelOverride + PlatformConfig.channel_overrides

- run: _resolve_model_for_channel, _get_system_prompt_for_channel, channel provider runtime

- tests: channel overrides + config guard for bare runner; conftest asyncio fix; slack/whatsapp warning filters

Made-with: Cursor
…ousResearch#1955)

- ChannelOverride + channel_overrides on PlatformConfig
- Resolve model/runtime: session /model, then channel_overrides, then global
- Thread/parent channel lookup; bridge discord.channel_overrides from YAML
- Drop unrelated test and delegate_tool changes from PR scope
@crazywriter1 crazywriter1 force-pushed the feat/1955-channel-overrides branch from 0f4b724 to f8c7345 Compare May 17, 2026 13:33
…ousResearch#1955)

- ChannelOverride + channel_overrides; session /model > channel > global
- Thread/parent lookup; YAML bridge for discord.channel_overrides
- Guard channel_overrides when config lacks platforms (test mocks)
- Add sampiyonyus@gmail.com to AUTHOR_MAP
@crazywriter1

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main and addressed the review feedback. Summary of what changed:

Scope cleanup

  • Dropped unrelated diffs: tests/conftest.py, tests/gateway/test_slack.py, tests/gateway/test_whatsapp_connect.py, tools/delegate_tool.py
  • PR is now 4 feature files (+ scripts/release.py for AUTHOR_MAP / check-attribution)

#1955 behavior

  • ChannelOverride + platforms.<name>.channel_overrides for model, provider, system_prompt
  • Resolution order: session /modelchannel_overrides → global (_resolve_gateway_model(user_config) in run_sync via _resolve_session_agent_runtime)
  • Lookup: chat_idthread_idparent_id (threads inherit parent channel when no thread-specific entry)
  • Top-level discord.channel_overrides in config.yaml (bridged like channel_prompts)
  • Explicit model on an override is preserved when provider is also set
  • channel_prompts unchanged (adapter / event.channel_prompt); no duplicate stacking
  • Guard when test mocks use config without platforms (getattr on channel_overrides lookup)

Tests

  • tests/gateway/test_config.py, tests/gateway/test_channel_overrides.py (including session vs channel priority and parent/thread inheritance)

CI note

  • Remaining CI failures do not touch this PR’s gateway files (e.g. ACP agent.json 0.13.0 vs pyproject 0.14.0 on main, telegram metadata, aiohttp trust_env mocks, env-specific ollama catalog). Happy to help split fixes if you want them in this PR.

PR description updated. Understand this may stay on hold until gateway basics settle — ready for another look when useful. @teknium1 @PaulBlackSwan

Resolve gateway/config.py conflicts: keep channel_overrides and main's
gateway_restart_notification YAML bridge (_grn + shared-key loop).
@benfeather

benfeather commented May 21, 2026

Copy link
Copy Markdown

It would be really nice if the per-channel options supported other platform options, using Discord as an example: certain channels could configure auto_thread, reactions, require_mention etc., separately from the global options

@ricatix

ricatix commented May 23, 2026

Copy link
Copy Markdown

+1. Hit this exact limitation today — single gateway, multiple Discord channels, each needing a different model. Without this, options are either separate gateway instances or manual /model every session. Would be great to see this merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: per-channel model and system prompt overrides for gateway platforms

7 participants