opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine

hermes --tui launches the native OpenTUI engine (Bun) when
HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config);
Ink stays the default and the shipping path is untouched.

- _resolve_tui_engine() (env > config > ink); refuses opentui on
  Windows/Termux (no Bun) -> falls back to ink with a notice.
- _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step).
- _bun_bin() with HERMES_BUN override.
- Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host
  must not bootstrap Node).
- Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun
  is JSC; the V8 flag errors/ignores).

Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI ->
real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
alt-glitch
2026-06-08 11:11:54 +00:00
parent 24f74eb888
commit 2bd9c9b881
741 changed files with 17733 additions and 79889 deletions
-181
View File
@@ -1794,108 +1794,6 @@ def test_resolve_api_key_provider_skips_unconfigured_anthropic(monkeypatch):
# ---------------------------------------------------------------------------
class TestTransientTransportRetry:
"""call_llm retries ONCE on the same provider for a transient transport
blip before escalating to the fallback chain.
Salvaged from PR #16587 (@ARegalado1). The original fixed only the
context-compression caller; this lives in call_llm so every auxiliary
task (compression, memory flush, title-gen, session-search, vision)
gets the same same-target retry, and the gate reuses the canonical
_is_connection_error detector.
"""
def _patches(self, client):
return (
patch(
"agent.auxiliary_client._resolve_task_provider_model",
return_value=("openrouter", "some-model", None, None, None),
),
patch(
"agent.auxiliary_client._get_cached_client",
return_value=(client, "some-model"),
),
patch(
"agent.auxiliary_client._validate_llm_response",
side_effect=lambda resp, _task: resp,
),
)
def test_retries_streaming_close_once_same_provider(self):
client = MagicMock()
client.base_url = "https://openrouter.ai/api/v1"
client.chat.completions.create.side_effect = [
Exception(
"peer closed connection without sending complete message body "
"(incomplete chunked read)"
),
{"ok": True},
]
p1, p2, p3 = self._patches(client)
with p1, p2, p3:
result = call_llm(task="compression", messages=[{"role": "user", "content": "hi"}])
assert result == {"ok": True}
# Same client called twice — no provider fallback needed.
assert client.chat.completions.create.call_count == 2
def test_retries_5xx_once_same_provider(self):
class _Err503(Exception):
status_code = 503
client = MagicMock()
client.base_url = "https://openrouter.ai/api/v1"
client.chat.completions.create.side_effect = [_Err503("upstream"), {"ok": True}]
p1, p2, p3 = self._patches(client)
with p1, p2, p3:
result = call_llm(task="compression", messages=[{"role": "user", "content": "hi"}])
assert result == {"ok": True}
assert client.chat.completions.create.call_count == 2
def test_does_not_retry_non_transient_400(self):
class _Err400(Exception):
status_code = 400
client = MagicMock()
client.base_url = "https://openrouter.ai/api/v1"
client.chat.completions.create.side_effect = _Err400("bad request")
p1, p2, p3 = self._patches(client)
with p1, p2, p3, pytest.raises(_Err400):
call_llm(task="compression", messages=[{"role": "user", "content": "hi"}])
# Non-transient: single attempt, no same-target retry.
assert client.chat.completions.create.call_count == 1
def test_second_transient_failure_escalates_to_fallback(self):
"""Two transient failures in a row exhaust the same-target retry and
fall through to the existing connection-error provider fallback."""
primary = MagicMock()
primary.base_url = "https://openrouter.ai/api/v1"
primary.chat.completions.create.side_effect = Exception(
"peer closed connection without sending complete message body"
)
fb_client = MagicMock()
fb_client.base_url = "https://api.openai.com/v1"
fb_client.chat.completions.create.return_value = {"fallback": True}
p1, p2, p3 = self._patches(primary)
with (
p1, p2, p3,
patch(
"agent.auxiliary_client._try_configured_fallback_chain",
return_value=(None, None, ""),
),
patch(
"agent.auxiliary_client._try_main_agent_model_fallback",
return_value=(fb_client, "fb-model", "openai"),
),
):
result = call_llm(task="compression", messages=[{"role": "user", "content": "hi"}])
assert result == {"fallback": True}
# Primary tried twice (initial + same-target retry), then fallback.
assert primary.chat.completions.create.call_count == 2
assert fb_client.chat.completions.create.call_count == 1
class TestIsConnectionError:
"""Tests for _is_connection_error detection."""
@@ -3791,82 +3689,3 @@ class TestAuxUnhealthyCache:
)
# After the 402, OpenRouter is in the unhealthy cache.
assert _is_provider_unhealthy("openrouter") is True
# ── auxiliary_max_tokens_param ──────────────────────────────────────────────
class TestAuxiliaryMaxTokensParam:
"""Verify the kwarg emitted by ``auxiliary_max_tokens_param`` across
URL / provider / model-name combinations. Regression cover: a custom
OpenAI-compatible endpoint serving ``gpt-5.x`` was silently getting
``max_tokens`` and 400-ing on ``unsupported_parameter``."""
def test_direct_openai_returns_max_completion_tokens(self):
with (
patch("agent.auxiliary_client._current_custom_base_url",
return_value="https://api.openai.com/v1"),
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
):
assert auxiliary_max_tokens_param(4096) == {"max_completion_tokens": 4096}
def test_local_endpoint_without_model_uses_max_tokens(self):
with (
patch("agent.auxiliary_client._current_custom_base_url",
return_value="http://localhost:11434/v1"),
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
):
assert auxiliary_max_tokens_param(4096) == {"max_tokens": 4096}
def test_openrouter_api_key_present_keeps_max_tokens_without_model_hint(self, monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-v1-test")
with (
patch("agent.auxiliary_client._current_custom_base_url",
return_value="https://openrouter.ai/api/v1"),
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
):
assert auxiliary_max_tokens_param(4096) == {"max_tokens": 4096}
# Model-name fallback — this is the regression guard.
def test_custom_endpoint_serving_gpt5_uses_max_completion_tokens(self):
"""Third-party gateway + gpt-5.x: name-based detection must kick in."""
with (
patch("agent.auxiliary_client._current_custom_base_url",
return_value="https://my-gateway.example.com/v1"),
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
):
assert auxiliary_max_tokens_param(4096, model="gpt-5.4") == {
"max_completion_tokens": 4096
}
def test_openrouter_serving_gpt4o_uses_max_completion_tokens(self, monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-v1-test")
with (
patch("agent.auxiliary_client._current_custom_base_url",
return_value="https://openrouter.ai/api/v1"),
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
):
assert auxiliary_max_tokens_param(4096, model="openai/gpt-4o-mini") == {
"max_completion_tokens": 4096
}
def test_custom_endpoint_serving_classic_llama_keeps_max_tokens(self):
with (
patch("agent.auxiliary_client._current_custom_base_url",
return_value="https://my-gateway.example.com/v1"),
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
):
assert auxiliary_max_tokens_param(4096, model="llama3-70b") == {
"max_tokens": 4096
}
def test_empty_model_falls_back_to_url_only(self):
"""No model hint → only the URL-based rule applies."""
with (
patch("agent.auxiliary_client._current_custom_base_url",
return_value="https://my-gateway.example.com/v1"),
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
):
assert auxiliary_max_tokens_param(4096, model="") == {"max_tokens": 4096}
assert auxiliary_max_tokens_param(4096, model=None) == {"max_tokens": 4096}