fix(gateway): scope context refs to runtime profile

This commit is contained in:
kshitijk4poor 2026-07-11 23:19:57 +05:30 committed by kshitij
parent 4df6e6280d
commit 37a942650f
3 changed files with 126 additions and 1 deletions

View file

@ -10613,6 +10613,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
_msg_cwd = os.environ.get("TERMINAL_CWD", os.path.expanduser("~"))
_msg_config_ctx = None
_msg_cfg = None
_msg_custom_providers = []
try:
_msg_cfg = _load_gateway_config()
_msg_model_cfg = _msg_cfg.get("model", {})
@ -10620,6 +10621,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
_msg_raw_ctx = _msg_model_cfg.get("context_length")
if _msg_raw_ctx is not None:
_msg_config_ctx = int(_msg_raw_ctx)
try:
from hermes_cli.config import get_compatible_custom_providers
_msg_custom_providers = get_compatible_custom_providers(_msg_cfg)
except Exception:
_msg_custom_providers = _msg_cfg.get("custom_providers") or []
except Exception:
pass
# Resolve the session's actual model/provider/base_url the
@ -10640,6 +10647,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
api_key=_msg_runtime.get("api_key") or "",
config_context_length=_msg_config_ctx,
provider=_msg_runtime.get("provider") or "",
custom_providers=_msg_custom_providers,
)
_ctx_result = await preprocess_context_references_async(
message_text,
@ -10663,6 +10671,30 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
return message_text
async def _prepare_profile_scoped_inbound_message_text(
self,
*,
event: MessageEvent,
source: SessionSource,
history: List[Dict[str, Any]],
session_key: Optional[str] = None,
) -> Optional[str]:
"""Run inbound preprocessing under the routed profile when multiplexed."""
if getattr(getattr(self, "config", None), "multiplex_profiles", False):
with _profile_runtime_scope(self._resolve_profile_home_for_source(source)):
return await self._prepare_inbound_message_text(
event=event,
source=source,
history=history,
session_key=session_key,
)
return await self._prepare_inbound_message_text(
event=event,
source=source,
history=history,
session_key=session_key,
)
def _consume_pending_native_image_paths(self, session_key: str) -> List[str]:
pending_native = getattr(self, "_pending_native_image_paths_by_session", None)
if not pending_native:
@ -11505,7 +11537,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
# attachments (documents, audio, etc.) are not sent to the vision
# tool even when they appear in the same message.
# -----------------------------------------------------------------
message_text = await self._prepare_inbound_message_text(
message_text = await self._prepare_profile_scoped_inbound_message_text(
event=event,
source=source,
history=history,

View file

@ -46,6 +46,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
# Auto-extracted from noreply emails + manual overrides
AUTHOR_MAP = {
"iganapolsky@gmail.com": "IgorGanapolsky", # PR #62125 salvage (compaction anti-thrash threshold verification)
"tturney1@gmail.com": "TheTom", # PR #62696 salvage (gateway: expand @ context references under runtime/session model resolution)
"wilsonkinyuam@gmail.com": "WilsonKinyua", # PR #62052 (tui: persist unflushed conversations on disconnect/restart)
"humphreysun98@gmail.com": "HumphreySun98", # PR #61142 salvage (web: null web/backend config value guards)
"sonxi@nous.local": "17324393074", # PR #53196 salvage (tools_config: known_plugin_toolsets null guard; commit under unlinked local identity)

View file

@ -18,6 +18,7 @@ the hygiene-compression block already uses) and must actually reach
"""
import logging
import threading
from contextlib import contextmanager
import pytest
@ -185,3 +186,94 @@ async def test_at_reference_resolves_model_via_session_runtime(monkeypatch):
assert captured_runtime_call.get("model") == "openai/gpt-4.1-mini"
assert captured_runtime_call.get("base_url") == "https://api.openai.com/v1"
assert captured_runtime_call.get("provider") == "openai"
@pytest.mark.asyncio
async def test_at_reference_uses_routed_profile_scope_when_multiplexed(monkeypatch, tmp_path):
"""Secondary-profile preprocessing must resolve inside that profile scope."""
runner = _make_runner()
runner.config.multiplex_profiles = True
source = _source()
source.profile = "secondary"
profile_home = tmp_path / "profiles" / "secondary"
seen = []
@contextmanager
def _scope(home):
seen.append(("enter", home))
try:
yield
finally:
seen.append(("exit", home))
async def _prepared(**kwargs):
seen.append(("prepared", kwargs["source"].profile))
return "expanded"
monkeypatch.setattr(gateway_run, "_profile_runtime_scope", _scope)
monkeypatch.setattr(runner, "_resolve_profile_home_for_source", lambda _source: profile_home)
monkeypatch.setattr(runner, "_prepare_inbound_message_text", _prepared)
result = await runner._prepare_profile_scoped_inbound_message_text(
event=MessageEvent(text="@file:note", source=source),
source=source,
history=[],
session_key="agent:secondary:telegram:dm:123",
)
assert result == "expanded"
assert seen == [
("enter", profile_home),
("prepared", "secondary"),
("exit", profile_home),
]
@pytest.mark.asyncio
async def test_at_reference_passes_compatible_custom_provider_context(monkeypatch):
"""Per-model custom-provider limits must bound context-reference injection."""
runner = _make_runner()
source = _source()
captured = {}
custom_providers = [{
"name": "private",
"base_url": "https://private.example/v1",
"models": {"private/model": {"context_length": 32768}},
}]
monkeypatch.setattr(
gateway_run,
"_load_gateway_config",
lambda: {"model": {"default": "private/model"}, "custom_providers": custom_providers},
)
monkeypatch.setattr(
gateway_run,
"_resolve_gateway_model",
lambda _cfg=None: "private/model",
)
monkeypatch.setattr(
gateway_run,
"_resolve_runtime_agent_kwargs",
lambda: {"provider": "custom:private", "api_key": "test", "base_url": "https://private.example/v1"},
)
import hermes_cli.config as config_mod
import agent.model_metadata as model_meta_mod
import agent.context_references as ctx_mod
monkeypatch.setattr(config_mod, "get_compatible_custom_providers", lambda _cfg: custom_providers)
async def _fake_get_context(_model, **kwargs):
captured["custom_providers"] = kwargs["custom_providers"]
return 32768
async def _passthrough(message, **_kwargs):
return ContextReferenceResult(message=message, original_message=message)
monkeypatch.setattr(model_meta_mod, "get_model_context_length_async", _fake_get_context)
monkeypatch.setattr(ctx_mod, "preprocess_context_references_async", _passthrough)
await runner._prepare_inbound_message_text(
event=MessageEvent(text="@file:note", source=source), source=source, history=[]
)
assert captured["custom_providers"] == custom_providers