Merge pull request #4371 from nesquena/stage-4364

Release v0.51.477 — QL (gateway runs-API routing now opt-in #4362)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-17 10:52:43 -07:00
commit d59cae5984
4 files changed
+104 -3

No files matched your search

+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.477] — 2026-06-17 — Release QL (gateway runs-API routing now opt-in)
### Fixed
- **Gateway chat no longer silently loses mid-conversation context (#4362).** When the WebUI was connected to a Hermes gateway that advertised approval/runs support, it routed *every* browser chat turn through the gateway's `/v1/runs` API, which creates an isolated Api_Server session per turn — so the agent lost the conversation's context between messages. Routing through the runs API is now gated behind an explicit opt-in (`HERMES_WEBUI_GATEWAY_USE_RUNS_API=1` env var, or `webui_gateway_use_runs_api: true` in config), defaulting to off. Normal gateway chat now stays on `/v1/chat/completions`, which preserves the session via `X-Hermes-Session-Id`. Operators who want the in-gateway-chat approval-events flow can re-enable it with the flag. Thanks @rodboev.
## [v0.51.476] — 2026-06-17 — Release QK (cross-provider model-pick + non-git update-status fixes)
### Fixed
+14 -1
View File
@@ -36,6 +36,7 @@ _STREAM_RUN_IDS: dict[str, str] = {}
_WEBUI_CHAT_BACKEND_ENV = "HERMES_WEBUI_CHAT_BACKEND"
_WEBUI_GATEWAY_BASE_URL_ENV = "HERMES_WEBUI_GATEWAY_BASE_URL"
_WEBUI_GATEWAY_API_KEY_ENV = "HERMES_WEBUI_GATEWAY_API_KEY"
_WEBUI_GATEWAY_USE_RUNS_API_ENV = "HERMES_WEBUI_GATEWAY_USE_RUNS_API"
_GATEWAY_CHAT_BACKENDS = {"gateway", "api_server", "api-server"}
@@ -83,6 +84,18 @@ def _gateway_api_key(environ: dict[str, str] | None = None) -> str:
).strip()
def _gateway_use_runs_api_enabled(config_data=None, environ: dict[str, str] | None = None) -> bool:
"""Return True only when the operator has explicitly opted into the runs API path."""
source = os.environ if environ is None else environ
cfg = config_data if isinstance(config_data, dict) else {}
raw = str(
source.get(_WEBUI_GATEWAY_USE_RUNS_API_ENV)
or cfg.get("webui_gateway_use_runs_api")
or ""
).strip().lower()
return raw in ("1", "true", "yes", "on")
def gateway_chat_config_status(config_data=None, environ: dict[str, str] | None = None) -> dict:
"""Return redacted Gateway-backed chat configuration status."""
mode = webui_chat_backend_mode(config_data, environ)
@@ -545,7 +558,7 @@ def _run_gateway_chat_streaming(
base_url = _gateway_base_url(cfg)
api_key = _gateway_api_key()
# Capability gate: use runs API when gateway advertises approval support.
_use_runs_api = gateway_supports_approval(base_url, api_key)
_use_runs_api = _gateway_use_runs_api_enabled(cfg) and gateway_supports_approval(base_url, api_key)
if _use_runs_api:
body_extras = {}
if model_provider:
+2 -2
View File
@@ -151,7 +151,7 @@ def test_gateway_runs_api_submission():
mock_session.pending_started_at = None
try:
with patch.dict("os.environ", {"HERMES_WEBUI_CHAT_BACKEND": "gateway"}):
with patch.dict("os.environ", {"HERMES_WEBUI_CHAT_BACKEND": "gateway", "HERMES_WEBUI_GATEWAY_USE_RUNS_API": "1"}):
with patch("api.gateway_chat.gateway_supports_approval", lambda *_args, **_kwargs: True), \
patch("api.gateway_chat._run_gateway_runs_api_streaming", fake_runs_streaming), \
patch("api.gateway_chat.get_session", return_value=mock_session), \
@@ -415,7 +415,7 @@ def test_gateway_runs_api_cancel_does_not_emit_empty_response():
return None, {}
try:
with patch.dict("os.environ", {"HERMES_WEBUI_CHAT_BACKEND": "gateway"}):
with patch.dict("os.environ", {"HERMES_WEBUI_CHAT_BACKEND": "gateway", "HERMES_WEBUI_GATEWAY_USE_RUNS_API": "1"}):
with patch("api.gateway_chat.gateway_supports_approval", return_value=True), \
patch("api.gateway_chat._run_gateway_runs_api_streaming", side_effect=fake_runs_streaming), \
patch("api.gateway_chat.get_session", return_value=mock_session):
+82
View File
@@ -18,6 +18,7 @@ from api.gateway_chat import (
_gateway_sse_reasoning_delta,
_gateway_stream_usage,
_gateway_tool_progress_event,
_gateway_use_runs_api_enabled,
gateway_chat_config_status,
webui_chat_backend_mode,
webui_gateway_chat_enabled,
@@ -824,3 +825,84 @@ def test_gateway_chat_worker_forwards_image_attachments_as_multimodal_parts(tmp_
assert content[0] == {"type": "text", "text": "What is in this image?"}
assert content[1]["type"] == "image_url"
assert content[1]["image_url"]["url"].startswith("data:image/png;base64,")
def test_gateway_use_runs_api_is_default_off():
for env in ({}, {"HERMES_WEBUI_GATEWAY_USE_RUNS_API": ""}):
assert _gateway_use_runs_api_enabled({}, env) is False
def test_gateway_use_runs_api_only_accepts_explicit_truthy_values():
for value in ("1", "true", "yes", "on", " True ", " ON "):
assert _gateway_use_runs_api_enabled({}, {"HERMES_WEBUI_GATEWAY_USE_RUNS_API": value}) is True
def test_gateway_use_runs_api_rejects_generic_truthy_strings():
for value in ("enabled", "gateway", "api_server", "absolutely"):
assert _gateway_use_runs_api_enabled({}, {"HERMES_WEBUI_GATEWAY_USE_RUNS_API": value}) is False
def test_gateway_use_runs_api_can_be_enabled_from_config():
assert _gateway_use_runs_api_enabled({"webui_gateway_use_runs_api": "true"}, {}) is True
assert _gateway_use_runs_api_enabled({"webui_gateway_use_runs_api": "1"}, {}) is True
def test_gateway_use_runs_api_env_wins_over_config():
assert _gateway_use_runs_api_enabled(
{"webui_gateway_use_runs_api": "true"},
{"HERMES_WEBUI_GATEWAY_USE_RUNS_API": "false"},
) is False
def test_gateway_worker_skips_runs_api_when_opt_in_absent():
"""Worker uses chat/completions even when gateway advertises approval support, unless opt-in is set."""
from unittest.mock import patch, MagicMock
from api.config import STREAMS, STREAMS_LOCK
from api.gateway_chat import _run_gateway_chat_streaming
events = []
q = MagicMock()
q.put_nowait = lambda item: events.append(item)
stream_id = "sid-optin-gate"
with STREAMS_LOCK:
STREAMS[stream_id] = q
sse_body = b'data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n'
def fake_urlopen(req, *, timeout=None):
assert "/v1/chat/completions" in req.full_url
resp = MagicMock()
resp.__iter__ = lambda s: iter(sse_body.split(b"\n"))
resp.__enter__ = lambda s: s
resp.__exit__ = lambda s, *a: None
return resp
import os
env_override = {"HERMES_WEBUI_CHAT_BACKEND": "gateway"}
env_without_opt_in = {
k: v for k, v in os.environ.items()
if k != "HERMES_WEBUI_GATEWAY_USE_RUNS_API"
}
env_without_opt_in.update(env_override)
try:
with patch.dict("os.environ", env_without_opt_in, clear=True):
with patch("api.gateway_chat.gateway_supports_approval", return_value=True), \
patch("urllib.request.urlopen", side_effect=fake_urlopen), \
patch("api.gateway_chat.get_session", return_value=MagicMock(
active_stream_id=stream_id, workspace="/tmp",
profile=None, context_messages=[], messages=[],
)):
_run_gateway_chat_streaming(
session_id="sess-optin",
msg_text="hi",
model="test",
workspace="/tmp",
stream_id=stream_id,
)
event_types = [e[0] for e in events if isinstance(e, tuple) and len(e) >= 2]
assert "token" in event_types, "expected a token event from chat/completions path"
assert "apperror" not in event_types, "runs API path fired unexpectedly"
finally:
with STREAMS_LOCK:
STREAMS.pop(stream_id, None)