mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 13:41:53 +00:00
feat(gateway): per-channel model and system prompt overrides (Fixes #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
This commit is contained in:
parent
902b0b70e4
commit
ebef73f6b8
4 changed files with 311 additions and 8 deletions
128
tests/gateway/test_channel_overrides.py
Normal file
128
tests/gateway/test_channel_overrides.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""Tests for per-channel model and system prompt overrides (Fixes #1955)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import (
|
||||
ChannelOverride,
|
||||
GatewayConfig,
|
||||
Platform,
|
||||
PlatformConfig,
|
||||
)
|
||||
from gateway.run import _get_channel_override, GatewayRunner
|
||||
|
||||
|
||||
class TestGetChannelOverride:
|
||||
def test_no_override_when_empty_config(self):
|
||||
config = GatewayConfig()
|
||||
assert _get_channel_override(config, Platform.DISCORD, "123") is None
|
||||
|
||||
def test_no_override_when_platform_not_configured(self):
|
||||
config = GatewayConfig(platforms={})
|
||||
assert _get_channel_override(config, Platform.DISCORD, "123") is None
|
||||
|
||||
def test_no_override_when_channel_not_in_overrides(self):
|
||||
config = GatewayConfig(
|
||||
platforms={
|
||||
Platform.DISCORD: PlatformConfig(
|
||||
enabled=True,
|
||||
channel_overrides={
|
||||
"999": ChannelOverride(model="openrouter/healer-alpha"),
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
assert _get_channel_override(config, Platform.DISCORD, "123") is None
|
||||
|
||||
def test_returns_override_when_channel_matches(self):
|
||||
ov = ChannelOverride(
|
||||
model="openrouter/healer-alpha",
|
||||
provider="openrouter",
|
||||
system_prompt="You are a summarizer.",
|
||||
)
|
||||
config = GatewayConfig(
|
||||
platforms={
|
||||
Platform.DISCORD: PlatformConfig(
|
||||
enabled=True,
|
||||
channel_overrides={"1234567890": ov},
|
||||
),
|
||||
},
|
||||
)
|
||||
result = _get_channel_override(config, Platform.DISCORD, "1234567890")
|
||||
assert result is not None
|
||||
assert result.model == "openrouter/healer-alpha"
|
||||
assert result.provider == "openrouter"
|
||||
assert result.system_prompt == "You are a summarizer."
|
||||
|
||||
def test_returns_override_when_chat_id_is_int_like(self):
|
||||
"""Caller may pass str(chat_id); override keys are normalized to str."""
|
||||
config = GatewayConfig(
|
||||
platforms={
|
||||
Platform.DISCORD: PlatformConfig(
|
||||
enabled=True,
|
||||
channel_overrides={"123": ChannelOverride(model="gpt-4")},
|
||||
),
|
||||
},
|
||||
)
|
||||
assert _get_channel_override(config, Platform.DISCORD, "123").model == "gpt-4"
|
||||
|
||||
|
||||
class TestResolveModelForChannel:
|
||||
def test_uses_channel_override_when_present(self):
|
||||
config = GatewayConfig(
|
||||
platforms={
|
||||
Platform.DISCORD: PlatformConfig(
|
||||
enabled=True,
|
||||
channel_overrides={
|
||||
"chan_1": ChannelOverride(model="anthropic/claude-opus-4.6"),
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = config
|
||||
model = runner._resolve_model_for_channel(Platform.DISCORD, "chan_1")
|
||||
assert model == "anthropic/claude-opus-4.6"
|
||||
|
||||
def test_falls_back_to_global_when_no_override(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"gateway.run._resolve_gateway_model",
|
||||
lambda: "global-model/default",
|
||||
)
|
||||
config = GatewayConfig(
|
||||
platforms={
|
||||
Platform.DISCORD: PlatformConfig(enabled=True, channel_overrides={}),
|
||||
},
|
||||
)
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = config
|
||||
model = runner._resolve_model_for_channel(Platform.DISCORD, "unknown_channel")
|
||||
assert model == "global-model/default"
|
||||
|
||||
|
||||
class TestGetSystemPromptForChannel:
|
||||
def test_uses_channel_override_when_present(self):
|
||||
config = GatewayConfig(
|
||||
platforms={
|
||||
Platform.DISCORD: PlatformConfig(
|
||||
enabled=True,
|
||||
channel_overrides={
|
||||
"chan_1": ChannelOverride(system_prompt="You are a coding assistant."),
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = config
|
||||
runner._ephemeral_system_prompt = "Global prompt"
|
||||
prompt = runner._get_system_prompt_for_channel(Platform.DISCORD, "chan_1")
|
||||
assert prompt == "You are a coding assistant."
|
||||
|
||||
def test_falls_back_to_global_when_no_override(self):
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.DISCORD: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = config
|
||||
runner._ephemeral_system_prompt = "Global prompt"
|
||||
prompt = runner._get_system_prompt_for_channel(Platform.DISCORD, "other")
|
||||
assert prompt == "Global prompt"
|
||||
|
|
@ -5,6 +5,7 @@ import os
|
|||
from unittest.mock import patch
|
||||
|
||||
from gateway.config import (
|
||||
ChannelOverride,
|
||||
GatewayConfig,
|
||||
HomeChannel,
|
||||
Platform,
|
||||
|
|
@ -89,6 +90,54 @@ class TestPlatformConfigRoundtrip:
|
|||
# extra; from_dict must honor it there too (mirrors _grn fallback).
|
||||
restored = PlatformConfig.from_dict({"extra": {"typing_indicator": False}})
|
||||
assert restored.typing_indicator is False
|
||||
def test_channel_overrides_roundtrip(self):
|
||||
pc = PlatformConfig(
|
||||
enabled=True,
|
||||
channel_overrides={
|
||||
"1234567890": ChannelOverride(
|
||||
model="openrouter/healer-alpha",
|
||||
provider="openrouter",
|
||||
system_prompt="You are a daily news summarizer.",
|
||||
),
|
||||
"9876543210": ChannelOverride(
|
||||
model="anthropic/claude-opus-4.6",
|
||||
provider="anthropic",
|
||||
system_prompt="You are a coding assistant.",
|
||||
),
|
||||
},
|
||||
)
|
||||
d = pc.to_dict()
|
||||
assert "channel_overrides" in d
|
||||
assert d["channel_overrides"]["1234567890"]["model"] == "openrouter/healer-alpha"
|
||||
assert d["channel_overrides"]["9876543210"]["system_prompt"] == "You are a coding assistant."
|
||||
restored = PlatformConfig.from_dict(d)
|
||||
assert restored.channel_overrides["1234567890"].model == "openrouter/healer-alpha"
|
||||
assert restored.channel_overrides["9876543210"].provider == "anthropic"
|
||||
|
||||
def test_channel_overrides_from_dict_normalizes_channel_id_to_str(self):
|
||||
"""YAML may have numeric channel IDs; we store as str."""
|
||||
data = {
|
||||
"enabled": True,
|
||||
"channel_overrides": {
|
||||
1234567890: {"model": "openrouter/healer-alpha"},
|
||||
},
|
||||
}
|
||||
pc = PlatformConfig.from_dict(data)
|
||||
assert "1234567890" in pc.channel_overrides
|
||||
assert pc.channel_overrides["1234567890"].model == "openrouter/healer-alpha"
|
||||
|
||||
|
||||
class TestChannelOverride:
|
||||
def test_from_dict_empty(self):
|
||||
assert ChannelOverride.from_dict({}).model is None
|
||||
assert ChannelOverride.from_dict(None).model is None
|
||||
|
||||
def test_to_dict_omits_none(self):
|
||||
ov = ChannelOverride(model="gpt-4", provider=None, system_prompt="Hi")
|
||||
d = ov.to_dict()
|
||||
assert d["model"] == "gpt-4"
|
||||
assert "provider" not in d
|
||||
assert d["system_prompt"] == "Hi"
|
||||
|
||||
|
||||
class TestGetConnectedPlatforms:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue