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:
+12
-73
@@ -194,71 +194,17 @@ class AgentNotice:
|
||||
id: Optional[str] = None
|
||||
|
||||
|
||||
# ── is_free_tier_model (local-data-only free-model check) ────────────────────
|
||||
|
||||
|
||||
def is_free_tier_model(model: str, base_url: str = "") -> bool:
|
||||
"""Return True when *model* is a Nous free-tier model, using ONLY local data.
|
||||
|
||||
Two signals, both zero-network:
|
||||
|
||||
1. The ``:free`` suffix — the canonical Nous free SKU marker (e.g.
|
||||
``nvidia/nemotron-3-ultra:free``). Free by construction on the API side
|
||||
(spend is forced to 0 for ``:free`` ids).
|
||||
2. A peek into the in-process pricing cache in ``hermes_cli.models``
|
||||
(populated when the model picker fetched ``/v1/models`` pricing for
|
||||
*base_url*). PEEK ONLY — a cache miss never triggers a fetch. This is
|
||||
CLI/TUI-session best-effort: gateway sessions never run the picker's
|
||||
pricing fetch, so suppression there rests entirely on the ``:free``
|
||||
suffix (which all Nous free SKUs carry).
|
||||
|
||||
Fail-open to False (the depleted notice still shows) on any error: wrongly
|
||||
showing the warning is recoverable noise; wrongly hiding it on a paid model
|
||||
would mask a real billing block.
|
||||
"""
|
||||
if not model:
|
||||
return False
|
||||
if model.endswith(":free"):
|
||||
return True
|
||||
if not base_url:
|
||||
return False
|
||||
try:
|
||||
from hermes_cli.models import _is_model_free, _pricing_cache
|
||||
|
||||
# Mirror get_pricing_for_provider's key normalization: the agent's
|
||||
# Nous base_url is /v1-suffixed (https://inference-api.nousresearch.com/v1)
|
||||
# but the picker keys _pricing_cache on the pre-/v1 root.
|
||||
key = base_url.rstrip("/")
|
||||
if key.endswith("/v1"):
|
||||
key = key[:-3].rstrip("/")
|
||||
pricing = _pricing_cache.get(key)
|
||||
if not pricing:
|
||||
return False
|
||||
return _is_model_free(model, pricing)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ── evaluate_credits_notices (pure reconciliation function) ──────────────────
|
||||
|
||||
|
||||
def evaluate_credits_notices(
|
||||
state: CreditsState,
|
||||
latch: dict,
|
||||
*,
|
||||
model_is_free: bool = False,
|
||||
) -> tuple[list[AgentNotice], list[str]]:
|
||||
"""Reconcile credits notices against the latch. Mutates ``latch`` IN PLACE.
|
||||
|
||||
latch = {"active": set[str], "seen_below_90": bool, "usage_band": Optional[int]}.
|
||||
|
||||
``model_is_free``: True when the session's active model is a Nous free-tier
|
||||
model (see :func:`is_free_tier_model`). Suppresses the ``credits.depleted``
|
||||
notice — a depleted account on a free model can keep inferencing, so the
|
||||
error banner is noise (and confuses free-tier users who never had credits).
|
||||
Suppression does NOT emit the "restored" success notice; that fires only on
|
||||
a genuine ``paid_access`` flip back to True.
|
||||
|
||||
Returns ``(to_show: list[AgentNotice], to_clear: list[str])``.
|
||||
Caller emits to_clear FIRST, then to_show.
|
||||
|
||||
@@ -338,11 +284,7 @@ def evaluate_credits_notices(
|
||||
active.discard("credits.grant_spent")
|
||||
|
||||
# ── depleted ─────────────────────────────────────────────────────────────
|
||||
# Suppressed while the active model is free: inference still works there,
|
||||
# so the error banner would just alarm users (free-tier users especially,
|
||||
# who never had paid credits to "lose").
|
||||
show_depleted = depleted_cond and not model_is_free
|
||||
if show_depleted and "credits.depleted" not in active:
|
||||
if depleted_cond and "credits.depleted" not in active:
|
||||
to_show.append(
|
||||
AgentNotice(
|
||||
text="✕ Credit access paused · run /usage for balance",
|
||||
@@ -353,23 +295,20 @@ def evaluate_credits_notices(
|
||||
)
|
||||
)
|
||||
active.add("credits.depleted")
|
||||
elif "credits.depleted" in active and not show_depleted:
|
||||
elif "credits.depleted" in active and not depleted_cond:
|
||||
to_clear.append("credits.depleted")
|
||||
active.discard("credits.depleted")
|
||||
if not depleted_cond:
|
||||
# Genuine recovery (paid_access flipped back True): also emit the
|
||||
# success notice. A clear caused by switching to a free model while
|
||||
# still depleted must NOT claim access was restored.
|
||||
to_show.append(
|
||||
AgentNotice(
|
||||
text="✓ Credit access restored",
|
||||
level="success",
|
||||
kind="ttl",
|
||||
ttl_ms=CREDITS_RESTORED_TTL_MS,
|
||||
key="credits.restored",
|
||||
id="credits.restored",
|
||||
)
|
||||
# Recovery: also emit the success notice
|
||||
to_show.append(
|
||||
AgentNotice(
|
||||
text="✓ Credit access restored",
|
||||
level="success",
|
||||
kind="ttl",
|
||||
ttl_ms=CREDITS_RESTORED_TTL_MS,
|
||||
key="credits.restored",
|
||||
id="credits.restored",
|
||||
)
|
||||
)
|
||||
|
||||
return (to_show, to_clear)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user