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:
@@ -124,11 +124,11 @@ class TestOpenRouterProfileParity:
|
||||
def test_reasoning_full_config(self, transport):
|
||||
rc = {"enabled": True, "effort": "high"}
|
||||
legacy = transport.build_kwargs(
|
||||
model="deepseek/deepseek-chat", messages=_msgs(), tools=None,
|
||||
model="anthropic/claude-sonnet-4.6", messages=_msgs(), tools=None,
|
||||
provider_profile=get_provider_profile("openrouter"), supports_reasoning=True, reasoning_config=rc,
|
||||
)
|
||||
profile = transport.build_kwargs(
|
||||
model="deepseek/deepseek-chat", messages=_msgs(), tools=None,
|
||||
model="anthropic/claude-sonnet-4.6", messages=_msgs(), tools=None,
|
||||
provider_profile=get_provider_profile("openrouter"),
|
||||
supports_reasoning=True, reasoning_config=rc,
|
||||
)
|
||||
@@ -136,11 +136,11 @@ class TestOpenRouterProfileParity:
|
||||
|
||||
def test_default_reasoning(self, transport):
|
||||
legacy = transport.build_kwargs(
|
||||
model="deepseek/deepseek-chat", messages=_msgs(), tools=None,
|
||||
model="anthropic/claude-sonnet-4.6", messages=_msgs(), tools=None,
|
||||
provider_profile=get_provider_profile("openrouter"), supports_reasoning=True,
|
||||
)
|
||||
profile = transport.build_kwargs(
|
||||
model="deepseek/deepseek-chat", messages=_msgs(), tools=None,
|
||||
model="anthropic/claude-sonnet-4.6", messages=_msgs(), tools=None,
|
||||
provider_profile=get_provider_profile("openrouter"),
|
||||
supports_reasoning=True,
|
||||
)
|
||||
|
||||
@@ -169,77 +169,6 @@ class TestOpenRouterProfile:
|
||||
)
|
||||
assert eb["reasoning"] == {"enabled": False}
|
||||
|
||||
def test_reasoning_disable_omitted_for_mandatory_anthropic(self):
|
||||
"""Reasoning-mandatory Anthropic models (4.6+/fable) reject any disable
|
||||
form: OpenRouter translates ``reasoning: {enabled: false}`` into
|
||||
Anthropic's ``thinking: {type: disabled}``, which 400s. The profile must
|
||||
omit ``reasoning`` so the model falls back to adaptive thinking instead.
|
||||
"""
|
||||
p = get_provider_profile("openrouter")
|
||||
for model in (
|
||||
"anthropic/claude-fable-5", # new named model
|
||||
"anthropic/claude-some-future-7", # unknown → default mandatory
|
||||
"anthropic/claude-opus-4.8",
|
||||
"anthropic/claude-opus-4.6",
|
||||
):
|
||||
for cfg in ({"enabled": False}, {"effort": "none"}):
|
||||
eb, _ = p.build_api_kwargs_extras(
|
||||
reasoning_config=cfg,
|
||||
supports_reasoning=True,
|
||||
model=model,
|
||||
)
|
||||
assert "reasoning" not in eb, (model, cfg, eb)
|
||||
|
||||
def test_reasoning_disable_kept_for_legacy_anthropic(self):
|
||||
"""Older Anthropic models still accept an explicit disable form, so the
|
||||
profile must keep forwarding it."""
|
||||
p = get_provider_profile("openrouter")
|
||||
for model in (
|
||||
"anthropic/claude-3.7-sonnet",
|
||||
"anthropic/claude-opus-4.5",
|
||||
"anthropic/claude-sonnet-4.5",
|
||||
):
|
||||
eb, _ = p.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": False},
|
||||
supports_reasoning=True,
|
||||
model=model,
|
||||
)
|
||||
assert eb["reasoning"] == {"enabled": False}, (model, eb)
|
||||
|
||||
def test_reasoning_disable_kept_for_non_anthropic(self):
|
||||
"""Non-Anthropic models (DeepSeek, Qwen, …) disable reasoning fine; the
|
||||
Anthropic-mandatory guard must not touch them."""
|
||||
p = get_provider_profile("openrouter")
|
||||
for model in ("deepseek/deepseek-chat", "qwen/qwen3-max", "openai/gpt-5.4"):
|
||||
eb, _ = p.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": False},
|
||||
supports_reasoning=True,
|
||||
model=model,
|
||||
)
|
||||
assert eb["reasoning"] == {"enabled": False}, (model, eb)
|
||||
|
||||
def test_reasoning_omitted_for_mandatory_anthropic_even_when_enabled(self):
|
||||
"""Reasoning-mandatory Anthropic models (4.6+/fable) use adaptive
|
||||
thinking — OpenRouter ignores reasoning.effort for them, and sending any
|
||||
reasoning field makes OpenRouter emit thinking.type.disabled on
|
||||
tool-continuation turns (whose assistant tool_calls carry no thinking
|
||||
block), 400ing every turn after the first tool call. The profile must
|
||||
omit reasoning entirely so the model defaults to adaptive.
|
||||
"""
|
||||
p = get_provider_profile("openrouter")
|
||||
for cfg in (
|
||||
{"enabled": True, "effort": "medium"},
|
||||
{"enabled": True, "effort": "xhigh"},
|
||||
{"effort": "high"},
|
||||
{"enabled": True},
|
||||
):
|
||||
eb, _ = p.build_api_kwargs_extras(
|
||||
reasoning_config=cfg,
|
||||
supports_reasoning=True,
|
||||
model="anthropic/claude-fable-5",
|
||||
)
|
||||
assert "reasoning" not in eb, (cfg, eb)
|
||||
|
||||
def test_default_reasoning(self):
|
||||
p = get_provider_profile("openrouter")
|
||||
eb, _ = p.build_api_kwargs_extras(supports_reasoning=True)
|
||||
@@ -291,121 +220,6 @@ class TestOpenRouterProfile:
|
||||
assert eb["reasoning"] == {"enabled": True, "effort": "high"}
|
||||
assert tl["extra_headers"]["x-grok-conv-id"] == "sess-123"
|
||||
|
||||
# --- reasoning-mandatory Anthropic effort → top-level verbosity (#43432) ---
|
||||
#
|
||||
# These models (Claude 4.6+ / fable / mythos-class) ignore
|
||||
# ``reasoning.effort`` and use adaptive thinking. OpenRouter honors the
|
||||
# requested effort on the top-level ``verbosity`` field instead (maps to
|
||||
# Anthropic ``output_config.effort``). The profile must route the existing
|
||||
# ``reasoning_config["effort"]`` there while still NEVER emitting a
|
||||
# ``reasoning`` field (which would 400 — see #42991). Gate every fixture on
|
||||
# the real predicate so this stays a behavior contract, not a name snapshot.
|
||||
|
||||
@staticmethod
|
||||
def _is_mandatory(model):
|
||||
import inspect
|
||||
p = get_provider_profile("openrouter")
|
||||
mod = inspect.getmodule(type(p))
|
||||
return mod._anthropic_reasoning_is_mandatory(model)
|
||||
|
||||
def test_mandatory_anthropic_effort_routes_to_verbosity(self):
|
||||
"""effort set + reasoning enabled → top-level verbosity == effort,
|
||||
and NO reasoning field in extra_body.
|
||||
|
||||
Covers the full real config range produced by
|
||||
``hermes_constants.parse_reasoning_effort`` —
|
||||
``VALID_REASONING_EFFORTS = (minimal, low, medium, high, xhigh)``.
|
||||
"""
|
||||
p = get_provider_profile("openrouter")
|
||||
model = "anthropic/claude-fable-5"
|
||||
assert self._is_mandatory(model) # fixture really is mandatory
|
||||
for effort in ("minimal", "low", "medium", "high", "xhigh"):
|
||||
eb, tl = p.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": True, "effort": effort},
|
||||
supports_reasoning=True,
|
||||
model=model,
|
||||
)
|
||||
assert tl["verbosity"] == effort, (effort, tl)
|
||||
assert "reasoning" not in eb, (effort, eb)
|
||||
|
||||
def test_mandatory_anthropic_effort_without_enabled_key_routes(self):
|
||||
"""effort present without an explicit ``enabled`` key still routes to
|
||||
verbosity (enabled defaults to True)."""
|
||||
p = get_provider_profile("openrouter")
|
||||
eb, tl = p.build_api_kwargs_extras(
|
||||
reasoning_config={"effort": "xhigh"},
|
||||
supports_reasoning=True,
|
||||
model="anthropic/claude-fable-5",
|
||||
)
|
||||
assert tl["verbosity"] == "xhigh"
|
||||
assert "reasoning" not in eb
|
||||
|
||||
def test_mandatory_anthropic_verbosity_is_value_agnostic_passthrough(self):
|
||||
"""The mapping passes the effort value through verbatim — it must NOT
|
||||
clamp or whitelist. ``xhigh`` is a real config value; ``max`` is not
|
||||
producible by ``parse_reasoning_effort`` today but OpenRouter accepts it
|
||||
for Claude (live-proven in #43432), so a forward value must survive
|
||||
rather than be silently dropped. The OpenAI SDK type only literals
|
||||
``low|medium|high`` but it's a TypedDict (no runtime validation), so the
|
||||
extended scale reaches the wire untouched."""
|
||||
p = get_provider_profile("openrouter")
|
||||
for effort in ("xhigh", "max"):
|
||||
_, tl = p.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": True, "effort": effort},
|
||||
supports_reasoning=True,
|
||||
model="anthropic/claude-fable-5",
|
||||
)
|
||||
assert tl["verbosity"] == effort
|
||||
|
||||
def test_mandatory_anthropic_no_verbosity_when_effort_absent(self):
|
||||
"""No effort / none / disabled → no verbosity emitted, so the model
|
||||
keeps its own adaptive default. Still no reasoning field."""
|
||||
p = get_provider_profile("openrouter")
|
||||
model = "anthropic/claude-fable-5"
|
||||
for cfg in (
|
||||
None,
|
||||
{},
|
||||
{"enabled": True},
|
||||
{"effort": "none"},
|
||||
{"enabled": True, "effort": "none"},
|
||||
{"enabled": False, "effort": "high"}, # explicitly disabled wins
|
||||
):
|
||||
eb, tl = p.build_api_kwargs_extras(
|
||||
reasoning_config=cfg,
|
||||
supports_reasoning=True,
|
||||
model=model,
|
||||
)
|
||||
assert "verbosity" not in tl, (cfg, tl)
|
||||
assert "reasoning" not in eb, (cfg, eb)
|
||||
|
||||
def test_non_mandatory_reasoning_model_unchanged_no_verbosity(self):
|
||||
"""Non-mandatory reasoning models (DeepSeek, Qwen, GPT) keep getting
|
||||
``reasoning`` in extra_body and never get a ``verbosity`` field — the
|
||||
new path must not touch them."""
|
||||
p = get_provider_profile("openrouter")
|
||||
for model in ("deepseek/deepseek-chat", "qwen/qwen3-max", "openai/gpt-5.4"):
|
||||
assert not self._is_mandatory(model) # fixture really is non-mandatory
|
||||
eb, tl = p.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": True, "effort": "high"},
|
||||
supports_reasoning=True,
|
||||
model=model,
|
||||
)
|
||||
assert eb["reasoning"] == {"enabled": True, "effort": "high"}, (model, eb)
|
||||
assert "verbosity" not in tl, (model, tl)
|
||||
|
||||
def test_mandatory_anthropic_verbosity_coexists_with_grok_header(self):
|
||||
"""A reasoning-mandatory Anthropic model is never a Grok model, but the
|
||||
top-level dict must remain a single merged dict — verify the verbosity
|
||||
path doesn't clobber the extra_headers slot used by Grok affinity."""
|
||||
p = get_provider_profile("openrouter")
|
||||
# mandatory anthropic + effort → verbosity, no extra_headers
|
||||
_, tl = p.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": True, "effort": "high"},
|
||||
supports_reasoning=True,
|
||||
model="anthropic/claude-fable-5",
|
||||
)
|
||||
assert tl == {"verbosity": "high"}
|
||||
|
||||
|
||||
class TestNousProfile:
|
||||
def test_tags(self):
|
||||
|
||||
@@ -160,7 +160,7 @@ class TestOpenRouterParity:
|
||||
"""OpenRouter passes the FULL reasoning_config dict, not just effort."""
|
||||
rc = {"enabled": True, "effort": "high"}
|
||||
kw = transport.build_kwargs(
|
||||
model="deepseek/deepseek-chat",
|
||||
model="anthropic/claude-sonnet-4.6",
|
||||
messages=_simple_messages(),
|
||||
tools=None,
|
||||
provider_profile=get_provider_profile("openrouter"),
|
||||
@@ -169,24 +169,10 @@ class TestOpenRouterParity:
|
||||
)
|
||||
assert kw["extra_body"]["reasoning"] == rc
|
||||
|
||||
def test_reasoning_omitted_for_mandatory_anthropic(self, transport):
|
||||
"""Adaptive-thinking Anthropic models (4.6+/fable) get NO reasoning
|
||||
field — sending one makes OpenRouter emit thinking.type.disabled on
|
||||
tool-replay turns, which the model 400s on."""
|
||||
kw = transport.build_kwargs(
|
||||
model="anthropic/claude-sonnet-4.6",
|
||||
messages=_simple_messages(),
|
||||
tools=None,
|
||||
provider_profile=get_provider_profile("openrouter"),
|
||||
supports_reasoning=True,
|
||||
reasoning_config={"enabled": True, "effort": "high"},
|
||||
)
|
||||
assert "reasoning" not in kw.get("extra_body", {})
|
||||
|
||||
def test_default_reasoning_when_no_config(self, transport):
|
||||
"""When supports_reasoning=True but no config, adds default."""
|
||||
kw = transport.build_kwargs(
|
||||
model="deepseek/deepseek-chat",
|
||||
model="anthropic/claude-sonnet-4.6",
|
||||
messages=_simple_messages(),
|
||||
tools=None,
|
||||
provider_profile=get_provider_profile("openrouter"),
|
||||
|
||||
Reference in New Issue
Block a user