Merge remote-tracking branch 'origin/master' into paperclip/pr-6222
This commit is contained in:
commit
39ef186a20
5 files changed
+496
-10
No files matched your search
@@ -17,6 +17,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Gateway-backed sessions can once again send with a MoA (Mixture-of-Agents) default model.** A legitimate gateway-default MoA send was being rejected as if it were a disallowed per-request override. The chat-start handler now distinguishes the two: a send is allowed only when the *configured* provider is MoA, the resolved model matches the configured default, and no explicit picker override was made — all evaluated against one request-owned config snapshot captured atomically, so a concurrent profile/config change can't flip the decision mid-request. Explicit `moa_config` overrides on gateway sessions still return 409. Thanks @rodboev. (#5869, #5853)
|
||||
|
||||
- **Content-search and sidebar polling are faster: run-journal summaries are now cached.** Repeated reads of an unchanged run journal reuse a cached summary instead of re-parsing the whole file, keyed on a complete filesystem signature (device, inode, size, mtime, and ctime) so the cache is always dropped the instant the journal is appended to, replaced, truncated, or recreated — including a same-size rewrite that preserves mtime. Bounded LRU; a first-write race can never pin a stale empty result. Thanks @sjungwon03. (#6291)
|
||||
|
||||
- **Live Stream: a settled Worklog now shows the side effects a turn performed (memory/skill saves).** The `state_saved` SSE event (memory + skill saves) was classified as an Assistant-Turn-Anchor side effect but the renderer-neutral scene projection discarded it, so the browser only flashed a toast and a reloaded Worklog lost that record. Anchor `artifacts`/`side_effects` are now projected into the settled `activity_scene_v1`. Purely additive — turns with no side effects render byte-identical (side-effect-only turns still don't force a Worklog), and the change is compatible with the stable-run-identity plumbing. Thanks @franksong2702. (#6204, #3400)
|
||||
|
||||
@@ -484,6 +484,25 @@ def get_config() -> dict:
|
||||
return _cfg_cache
|
||||
|
||||
|
||||
def get_config_snapshot() -> dict:
|
||||
"""Return a request-owned config snapshot captured under the cache lock."""
|
||||
with _cfg_lock:
|
||||
config_path = _get_config_path()
|
||||
try:
|
||||
current_mtime = config_path.stat().st_mtime
|
||||
except OSError:
|
||||
current_mtime = 0.0
|
||||
path_changed = _cfg_path != config_path
|
||||
mtime_stale = current_mtime != _cfg_mtime
|
||||
if not _cfg_cache or path_changed or (mtime_stale and not _cfg_has_in_memory_overrides()):
|
||||
_refresh_config_cache(config_path)
|
||||
try:
|
||||
active_cfg = cfg if cfg is not _cfg_cache else _cfg_cache
|
||||
except NameError:
|
||||
active_cfg = _cfg_cache
|
||||
return copy.deepcopy(active_cfg)
|
||||
|
||||
|
||||
def get_webui_session_save_mode(config_data: dict | None = None) -> str:
|
||||
"""Return the validated first-turn session persistence mode.
|
||||
|
||||
|
||||
+26
-7
@@ -2823,7 +2823,9 @@ from api.config import (
|
||||
set_reasoning_display,
|
||||
set_reasoning_effort,
|
||||
create_stream_channel,
|
||||
get_config,
|
||||
get_webui_session_save_mode,
|
||||
get_config_snapshot,
|
||||
STREAM_GOAL_RELATED,
|
||||
PENDING_GOAL_CONTINUATION,
|
||||
_get_config_path,
|
||||
@@ -21260,6 +21262,7 @@ def _start_run(
|
||||
route: str,
|
||||
diag=None,
|
||||
moa_config=None,
|
||||
gateway_chat_enabled: bool | None = None,
|
||||
):
|
||||
"""Shared start-run helper for /api/chat/start and start_session_turn.
|
||||
|
||||
@@ -21300,6 +21303,7 @@ def _start_run(
|
||||
diag=diag,
|
||||
source=request.source or source,
|
||||
moa_config=moa_config,
|
||||
external_runtime_owned=gateway_chat_enabled,
|
||||
)
|
||||
|
||||
def _legacy_adapter_factory():
|
||||
@@ -21340,7 +21344,7 @@ def _start_run(
|
||||
diag=diag,
|
||||
source=source,
|
||||
moa_config=moa_config,
|
||||
external_runtime_owned=webui_gateway_chat_enabled(get_config()),
|
||||
external_runtime_owned=gateway_chat_enabled,
|
||||
)
|
||||
|
||||
|
||||
@@ -22066,7 +22070,8 @@ def _handle_chat_start(handler, body, diag=None):
|
||||
_pp_provider, _pp_default, _pp_cfg = _read_profile_model_config(s, requested_provider)
|
||||
explicit_model_pick = bool(body.get("explicit_model_pick"))
|
||||
moa_config = None
|
||||
gateway_chat_enabled = webui_gateway_chat_enabled(get_config())
|
||||
config_snapshot = get_config_snapshot()
|
||||
gateway_chat_enabled = webui_gateway_chat_enabled(config_snapshot)
|
||||
if body.get("moa_config"):
|
||||
if gateway_chat_enabled:
|
||||
return bad(handler, "MoA override is unavailable on gateway-backed sessions", 409)
|
||||
@@ -22113,9 +22118,25 @@ def _handle_chat_start(handler, body, diag=None):
|
||||
explicit_model_pick=explicit_model_pick,
|
||||
profile_provider=catalog_profile_provider,
|
||||
)
|
||||
if model_provider == "moa" and moa_config is None:
|
||||
if webui_gateway_chat_enabled(get_config()):
|
||||
if model_provider == "moa" and gateway_chat_enabled:
|
||||
from api.config import get_effective_default_model
|
||||
|
||||
model_config = config_snapshot.get("model") if isinstance(config_snapshot, dict) else None
|
||||
configured_default, configured_default_provider, configured_default_is_moa = (
|
||||
_moa_fast_path_model_state(get_effective_default_model(config_snapshot))
|
||||
)
|
||||
configured_provider = _clean_session_model_provider(
|
||||
model_config.get("provider") if isinstance(model_config, dict) else None
|
||||
)
|
||||
if configured_provider is None and configured_default_is_moa:
|
||||
configured_provider = configured_default_provider
|
||||
if (
|
||||
configured_provider != "moa"
|
||||
or model != configured_default
|
||||
or explicit_model_pick
|
||||
):
|
||||
return bad(handler, "MoA override is unavailable on gateway-backed sessions", 409)
|
||||
elif model_provider == "moa" and moa_config is None:
|
||||
from api.commands import resolve_moa_config
|
||||
|
||||
try:
|
||||
@@ -22136,6 +22157,7 @@ def _handle_chat_start(handler, body, diag=None):
|
||||
"source": "webui",
|
||||
"route": "/api/chat/start",
|
||||
"diag": diag,
|
||||
"gateway_chat_enabled": gateway_chat_enabled,
|
||||
}
|
||||
if not gateway_chat_enabled and moa_config is not None:
|
||||
start_run_kwargs["moa_config"] = moa_config
|
||||
@@ -25744,9 +25766,6 @@ def _handle_session_import(handler, body):
|
||||
return j(handler, {"ok": True, "session": s.compact() | {"messages": s.messages}})
|
||||
|
||||
|
||||
# ── MCP Server helpers ──
|
||||
from api.config import get_config, _save_yaml_config_file, _get_config_path, reload_config
|
||||
|
||||
def _mask_secrets(obj):
|
||||
"""Mask sensitive values in env vars and headers."""
|
||||
if not isinstance(obj, dict):
|
||||
|
||||
@@ -595,8 +595,11 @@ def test_gateway_owned_start_run_bypasses_local_runtime_barrier(monkeypatch):
|
||||
captured = {}
|
||||
monkeypatch.setattr("api.runtime_adapter.runtime_adapter_enabled", lambda: False)
|
||||
monkeypatch.setattr("api.runtime_adapter.runtime_adapter_runner_enabled", lambda: False)
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
monkeypatch.setattr(routes, "get_config", lambda: {})
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"get_config",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("_start_run reread shared config")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"ensure_agent_runtime_current",
|
||||
@@ -621,6 +624,7 @@ def test_gateway_owned_start_run_bypasses_local_runtime_barrier(monkeypatch):
|
||||
normalized_model=False,
|
||||
source="webui",
|
||||
route="/api/chat/start",
|
||||
gateway_chat_enabled=True,
|
||||
)
|
||||
|
||||
assert response["stream_id"] == "gateway-stream-1"
|
||||
|
||||
@@ -12,6 +12,40 @@ import pytest
|
||||
from tests.conftest import TEST_BASE, requires_agent_modules
|
||||
|
||||
|
||||
def test_config_snapshot_waits_for_reload_lock(monkeypatch, tmp_path):
|
||||
"""Snapshot capture cannot run concurrently with cache replacement."""
|
||||
import threading
|
||||
|
||||
from api import config
|
||||
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text("{}", encoding="utf-8")
|
||||
shared_config = {"chat_backend": "gateway"}
|
||||
monkeypatch.setattr(config, "_cfg_cache", shared_config)
|
||||
monkeypatch.setattr(config, "cfg", shared_config)
|
||||
monkeypatch.setattr(config, "_cfg_path", config_path)
|
||||
monkeypatch.setattr(config, "_cfg_mtime", config_path.stat().st_mtime)
|
||||
monkeypatch.setattr(config, "_cfg_fingerprint", config._fingerprint_config(shared_config))
|
||||
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
|
||||
|
||||
complete = threading.Event()
|
||||
result = {}
|
||||
|
||||
def read_snapshot():
|
||||
result["snapshot"] = config.get_config_snapshot()
|
||||
complete.set()
|
||||
|
||||
with config._cfg_lock:
|
||||
reader = threading.Thread(target=read_snapshot)
|
||||
reader.start()
|
||||
assert not complete.wait(0.1)
|
||||
reader.join(timeout=1)
|
||||
|
||||
assert complete.is_set()
|
||||
assert result["snapshot"] == shared_config
|
||||
assert result["snapshot"] is not shared_config
|
||||
|
||||
|
||||
def _install_fake_moa_config(monkeypatch, *, default_preset="moa-default", usage_text="Usage: /moa <prompt>"):
|
||||
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
||||
# monkeypatch.setattr restores the REAL hermes_cli.__path__ on teardown;
|
||||
@@ -174,7 +208,7 @@ def test_moa_gateway_chat_start_fails_closed(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(routes, "get_session", lambda _sid: _Session())
|
||||
monkeypatch.setattr(routes, "_resolve_chat_workspace_with_recovery", lambda _s, _w: str(tmp_path))
|
||||
monkeypatch.setattr(routes, "_start_run", start_run)
|
||||
monkeypatch.setattr(routes, "get_config", lambda: {"chat_backend": "gateway"})
|
||||
monkeypatch.setattr(routes, "get_config_snapshot", lambda: {"chat_backend": "gateway"})
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
monkeypatch.setattr(commands, "resolve_moa_config", resolve_moa_config)
|
||||
|
||||
@@ -192,3 +226,411 @@ def test_moa_gateway_chat_start_fails_closed(monkeypatch, tmp_path):
|
||||
body = json.loads(handler.wfile.getvalue().decode("utf-8"))
|
||||
assert handler.status == 409
|
||||
assert body["error"] == "MoA override is unavailable on gateway-backed sessions"
|
||||
|
||||
|
||||
def test_moa_gateway_configured_default_reaches_start_run(monkeypatch, tmp_path):
|
||||
"""Gateway sessions may use only the configured MoA default."""
|
||||
import api.routes as routes
|
||||
|
||||
class _Handler:
|
||||
def __init__(self):
|
||||
import io
|
||||
|
||||
self.status = None
|
||||
self.response_headers = []
|
||||
self.wfile = io.BytesIO()
|
||||
|
||||
def send_response(self, status):
|
||||
self.status = status
|
||||
|
||||
def send_header(self, key, value):
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
self.response_headers.append(("__end__", ""))
|
||||
|
||||
class _Session:
|
||||
session_id = "sess-moa-gateway-default"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "moa"
|
||||
profile = "default"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
|
||||
captured = {}
|
||||
|
||||
def start_run(*_args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda _sid: _Session())
|
||||
monkeypatch.setattr(routes, "_resolve_chat_workspace_with_recovery", lambda _s, _w: str(tmp_path))
|
||||
monkeypatch.setattr(routes, "_start_run", start_run)
|
||||
for name in ("HERMES_MODEL", "OPENAI_MODEL", "LLM_MODEL"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
config_snapshot = {
|
||||
"chat_backend": "gateway",
|
||||
"model": {"provider": "moa", "default": "@moa:moa-configured"},
|
||||
}
|
||||
monkeypatch.setattr(routes, "get_config_snapshot", lambda: config_snapshot)
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
|
||||
routes._handle_chat_start(
|
||||
_Handler(),
|
||||
{
|
||||
"session_id": "sess-moa-gateway-default",
|
||||
"message": "diagnose issue",
|
||||
"workspace": str(tmp_path),
|
||||
"model": "moa/moa-configured",
|
||||
"model_provider": "moa",
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["model"] == "moa-configured"
|
||||
assert captured["model_provider"] == "moa"
|
||||
assert "moa_config" not in captured
|
||||
|
||||
|
||||
def test_moa_gateway_string_model_config_reaches_start_run(monkeypatch, tmp_path):
|
||||
"""String-form gateway defaults may also authorize MoA sends."""
|
||||
import api.routes as routes
|
||||
|
||||
class _Handler:
|
||||
def __init__(self):
|
||||
import io
|
||||
|
||||
self.status = None
|
||||
self.response_headers = []
|
||||
self.wfile = io.BytesIO()
|
||||
|
||||
def send_response(self, status):
|
||||
self.status = status
|
||||
|
||||
def send_header(self, key, value):
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
self.response_headers.append(("__end__", ""))
|
||||
|
||||
class _Session:
|
||||
session_id = "sess-moa-gateway-string-default"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "moa"
|
||||
profile = "default"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
|
||||
captured = {}
|
||||
|
||||
def start_run(*_args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda _sid: _Session())
|
||||
monkeypatch.setattr(routes, "_resolve_chat_workspace_with_recovery", lambda _s, _w: str(tmp_path))
|
||||
monkeypatch.setattr(routes, "_start_run", start_run)
|
||||
for name in ("HERMES_MODEL", "OPENAI_MODEL", "LLM_MODEL"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
monkeypatch.setattr(routes, "get_config_snapshot", lambda: {"chat_backend": "gateway", "model": "@moa:moa-configured"})
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
|
||||
routes._handle_chat_start(
|
||||
_Handler(),
|
||||
{
|
||||
"session_id": "sess-moa-gateway-string-default",
|
||||
"message": "diagnose issue",
|
||||
"workspace": str(tmp_path),
|
||||
"model": "moa/moa-configured",
|
||||
"model_provider": "moa",
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["model"] == "moa-configured"
|
||||
assert captured["model_provider"] == "moa"
|
||||
assert captured["gateway_chat_enabled"] is True
|
||||
assert "moa_config" not in captured
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("initial_config", "mutated_config", "expected_status"),
|
||||
[
|
||||
(
|
||||
{"chat_backend": "gateway", "model": {"provider": "openai", "default": "gpt-5.5"}},
|
||||
{"chat_backend": "gateway", "model": {"provider": "moa", "default": "@moa:moa-configured"}},
|
||||
409,
|
||||
),
|
||||
(
|
||||
{"chat_backend": "gateway", "model": {"provider": "moa", "default": "@moa:moa-configured"}},
|
||||
{"chat_backend": "gateway", "model": {"provider": "openai", "default": "gpt-5.5"}},
|
||||
200,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_moa_gateway_authorization_uses_request_owned_config(
|
||||
monkeypatch, tmp_path, initial_config, mutated_config, expected_status
|
||||
):
|
||||
"""Gateway MoA authorization stays on the request's config snapshot."""
|
||||
import io
|
||||
|
||||
import api.routes as routes
|
||||
|
||||
class _Handler:
|
||||
def __init__(self):
|
||||
self.status = None
|
||||
self.response_headers = []
|
||||
self.wfile = io.BytesIO()
|
||||
|
||||
def send_response(self, status):
|
||||
self.status = status
|
||||
|
||||
def send_header(self, key, value):
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
self.response_headers.append(("__end__", ""))
|
||||
|
||||
class _Session:
|
||||
session_id = "sess-moa-gateway-config-race"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "moa"
|
||||
profile = "default"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
|
||||
captured = {}
|
||||
config_snapshot = initial_config
|
||||
original_resolve = routes._resolve_compatible_session_model_state
|
||||
|
||||
def resolve_and_mutate(*args, **kwargs):
|
||||
result = original_resolve(*args, **kwargs)
|
||||
config_snapshot.clear()
|
||||
config_snapshot.update(mutated_config)
|
||||
return result
|
||||
|
||||
def start_run(*_args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda _sid: _Session())
|
||||
monkeypatch.setattr(routes, "_resolve_chat_workspace_with_recovery", lambda _s, _w: str(tmp_path))
|
||||
monkeypatch.setattr(routes, "_start_run", start_run)
|
||||
monkeypatch.setattr(routes, "_resolve_compatible_session_model_state", resolve_and_mutate)
|
||||
for name in ("HERMES_MODEL", "OPENAI_MODEL", "LLM_MODEL"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
monkeypatch.setattr(routes, "get_config_snapshot", lambda: routes.copy.deepcopy(config_snapshot))
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
|
||||
handler = _Handler()
|
||||
routes._handle_chat_start(
|
||||
handler,
|
||||
{
|
||||
"session_id": "sess-moa-gateway-config-race",
|
||||
"message": "diagnose issue",
|
||||
"workspace": str(tmp_path),
|
||||
"model": "moa/moa-configured",
|
||||
"model_provider": "moa",
|
||||
},
|
||||
)
|
||||
|
||||
assert handler.status == expected_status
|
||||
if expected_status == 200:
|
||||
assert captured["gateway_chat_enabled"] is True
|
||||
else:
|
||||
assert not captured
|
||||
|
||||
|
||||
def test_moa_gateway_explicit_configured_default_fails_closed(monkeypatch, tmp_path):
|
||||
"""A browser picker cannot select the gateway's MoA default."""
|
||||
import api.routes as routes
|
||||
|
||||
class _Handler:
|
||||
def __init__(self):
|
||||
import io
|
||||
|
||||
self.status = None
|
||||
self.response_headers = []
|
||||
self.wfile = io.BytesIO()
|
||||
|
||||
def send_response(self, status):
|
||||
self.status = status
|
||||
|
||||
def send_header(self, key, value):
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
self.response_headers.append(("__end__", ""))
|
||||
|
||||
class _Session:
|
||||
session_id = "sess-moa-gateway-picker"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "moa"
|
||||
profile = "default"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
|
||||
def start_run(*_args, **_kwargs):
|
||||
raise AssertionError("explicit MoA picker selection must fail before starting a run")
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda _sid: _Session())
|
||||
monkeypatch.setattr(routes, "_resolve_chat_workspace_with_recovery", lambda _s, _w: str(tmp_path))
|
||||
monkeypatch.setattr(routes, "_start_run", start_run)
|
||||
for name in ("HERMES_MODEL", "OPENAI_MODEL", "LLM_MODEL"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
config_snapshot = {
|
||||
"chat_backend": "gateway",
|
||||
"model": {"provider": "moa", "default": "@moa:moa-configured"},
|
||||
}
|
||||
monkeypatch.setattr(routes, "get_config_snapshot", lambda: config_snapshot)
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
|
||||
handler = _Handler()
|
||||
routes._handle_chat_start(
|
||||
handler,
|
||||
{
|
||||
"session_id": "sess-moa-gateway-picker",
|
||||
"message": "diagnose issue",
|
||||
"workspace": str(tmp_path),
|
||||
"model": "moa/moa-configured",
|
||||
"model_provider": "moa",
|
||||
"explicit_model_pick": True,
|
||||
},
|
||||
)
|
||||
|
||||
body = json.loads(handler.wfile.getvalue().decode("utf-8"))
|
||||
assert handler.status == 409
|
||||
assert body["error"] == "MoA override is unavailable on gateway-backed sessions"
|
||||
|
||||
|
||||
def test_moa_gateway_spoofed_provider_fails_closed(monkeypatch, tmp_path):
|
||||
"""A client cannot claim MoA for a model outside the configured default."""
|
||||
import api.routes as routes
|
||||
|
||||
class _Handler:
|
||||
def __init__(self):
|
||||
import io
|
||||
|
||||
self.status = None
|
||||
self.response_headers = []
|
||||
self.wfile = io.BytesIO()
|
||||
|
||||
def send_response(self, status):
|
||||
self.status = status
|
||||
|
||||
def send_header(self, key, value):
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
self.response_headers.append(("__end__", ""))
|
||||
|
||||
class _Session:
|
||||
session_id = "sess-moa-gateway-spoofed"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "openai-codex"
|
||||
profile = "default"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
|
||||
def start_run(*_args, **_kwargs):
|
||||
raise AssertionError("spoofed MoA provider must fail before starting a run")
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda _sid: _Session())
|
||||
monkeypatch.setattr(routes, "_resolve_chat_workspace_with_recovery", lambda _s, _w: str(tmp_path))
|
||||
monkeypatch.setattr(routes, "_start_run", start_run)
|
||||
for name in ("HERMES_MODEL", "OPENAI_MODEL", "LLM_MODEL"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
config_snapshot = {
|
||||
"chat_backend": "gateway",
|
||||
"model": {"provider": "moa", "default": "@moa:moa-configured"},
|
||||
}
|
||||
monkeypatch.setattr(routes, "get_config_snapshot", lambda: config_snapshot)
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
|
||||
handler = _Handler()
|
||||
routes._handle_chat_start(
|
||||
handler,
|
||||
{
|
||||
"session_id": "sess-moa-gateway-spoofed",
|
||||
"message": "diagnose issue",
|
||||
"workspace": str(tmp_path),
|
||||
"model": "moa/moa-spoofed",
|
||||
"model_provider": "moa",
|
||||
},
|
||||
)
|
||||
|
||||
body = json.loads(handler.wfile.getvalue().decode("utf-8"))
|
||||
assert handler.status == 409
|
||||
assert body["error"] == "MoA override is unavailable on gateway-backed sessions"
|
||||
|
||||
|
||||
def test_gateway_non_moa_model_reaches_start_run(monkeypatch, tmp_path):
|
||||
"""Gateway sends keep their non-MoA provider when the default uses MoA."""
|
||||
import api.routes as routes
|
||||
|
||||
class _Handler:
|
||||
def __init__(self):
|
||||
import io
|
||||
|
||||
self.status = None
|
||||
self.response_headers = []
|
||||
self.wfile = io.BytesIO()
|
||||
|
||||
def send_response(self, status):
|
||||
self.status = status
|
||||
|
||||
def send_header(self, key, value):
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
self.response_headers.append(("__end__", ""))
|
||||
|
||||
class _Session:
|
||||
session_id = "sess-gateway-non-moa"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "openai-codex"
|
||||
profile = "default"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
|
||||
captured = {}
|
||||
|
||||
def start_run(*_args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", lambda _sid: _Session())
|
||||
monkeypatch.setattr(routes, "_resolve_chat_workspace_with_recovery", lambda _s, _w: str(tmp_path))
|
||||
monkeypatch.setattr(routes, "_start_run", start_run)
|
||||
for name in ("HERMES_MODEL", "OPENAI_MODEL", "LLM_MODEL"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
config_snapshot = {
|
||||
"chat_backend": "gateway",
|
||||
"model": {"provider": "moa", "default": "@moa:moa-configured"},
|
||||
}
|
||||
monkeypatch.setattr(routes, "get_config_snapshot", lambda: config_snapshot)
|
||||
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: True)
|
||||
|
||||
routes._handle_chat_start(
|
||||
_Handler(),
|
||||
{
|
||||
"session_id": "sess-gateway-non-moa",
|
||||
"message": "diagnose issue",
|
||||
"workspace": str(tmp_path),
|
||||
"model": "gpt-5.5",
|
||||
"model_provider": "openai-codex",
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["model_provider"] == "openai-codex"
|
||||
assert "moa_config" not in captured
|
||||
Reference in new issue
Block a user