feat: per-channel model and system prompt overrides for gateway platforms (Fixes #1955)#1991
feat: per-channel model and system prompt overrides for gateway platforms (Fixes #1955)#1991crazywriter1 wants to merge 4 commits into
Conversation
…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
|
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:
Will re-review once the current foundational work stabilizes. The issue (#1955) stays open. |
|
Thanks for the review. @teknium1 I’ll address the comments and update the PR for re-review. |
|
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 The |
PaulBlackSwan
left a comment
There was a problem hiding this comment.
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/config — mirrors 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_namesToolset 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 objectwould 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 None7. Tests I'd want to see
The 1215-passing-tests claim is great. I'd add:
- Provider override path (
_resolve_runtime_agent_kwargs_for_provideris brand new but only indirectly tested) — covers theif channel_override and channel_override.provider:branch. - Precedence: an integration-ish test where both
_session_model_overrides[session_key]andchannel_overrides[chat_id]are set — confirms the documented "session > channel > global" order matches the code. - Combined
combined_ephemeralwhen channel override hassystem_promptAND 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.
|
@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. |
|
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
0f4b724 to
f8c7345
Compare
…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
|
Rebased onto latest Scope cleanup
#1955 behavior
Tests
CI note
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).
|
It would be really nice if the per-channel options supported other platform options, using Discord as an example: certain channels could configure |
|
+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 |
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
channel_overridesper platform — keys are channel/thread IDs; values are optionalmodel,provider,system_prompt./model→channel_overrides→ global default.chat_id→thread_id→parent_id(threads inherit parent channel when no thread-specific entry).providerresolves credentials viaresolve_runtime_provider; explicitmodelis kept when both are set.channel_overrides.system_promptinrun_sync; existingchannel_promptsvia adapter unchanged (not duplicated).platforms.discord.channel_overridesor top-leveldiscord.channel_overrides(same bridge style aschannel_prompts).main. Scope is 4 files only (removeddelegate_tooland unrelated test-only diffs).Changes
gateway/config.py—ChannelOverride,PlatformConfig.channel_overrides, roundtrip + YAML bridgegateway/run.py—_get_channel_override, channel layer in_resolve_session_agent_runtime(user_config),run_syncintegration, thread/parent lookup, safe when test mocks lackconfig.platformstests/gateway/test_config.py— config roundtrip + top-level YAML bridge testtests/gateway/test_channel_overrides.py— lookup, model/prompt, priority (session > channel > global), parent/thread inheritanceExample config
Or under
platforms:Testing
discord.channel_overridesloaded from top-level config.yaml