fix(tui-gateway): report Fast only where the route accepts the priority tier

A profile-wide agent.service_tier: fast reaches every new session,
including local llama.cpp ones. The request builders already drop the fast
parameter on routes that do not bill for it, but session.info reported
fast: true whenever the tier was priority. The desktop then kept the Fast
switch visible so it could be turned off, tagged the model row Fast and
appended "· Fast" to the pill for a local model.

session.info now asks resolve_fast_mode_overrides, the gate the request
builders use, before it reports fast. While a model switch is pending, the
new pick decides, because the agent's base URL still belongs to the old
route. service_tier is still reported as stored. The TUI status bar reads
info.fast alone.
This commit is contained in:
emozilla
2026-09-25 22:08:05 -04:00
parent 9c1ef17550
commit fd602278c7
3 changed files with 62 additions and 2 deletions

View File

@@ -121,3 +121,44 @@ class TestConfigGetFastSessionScope:
with patch.dict(server._sessions, {"s6": session}, clear=False):
resp = _get({"key": "fast", "session_id": "s6"})
assert resp["result"]["value"] == "fast"
class TestSessionInfoFastFollowsTheRoute:
"""``session.info`` reports Fast only where the priority tier reaches the wire. A profile-wide
``service_tier: fast`` on a local model sends nothing, so the Fast switch and label stay hidden."""
@staticmethod
def _info(**agent_fields) -> dict:
agent = SimpleNamespace(**{
"reasoning_config": None, "service_tier": "priority", "request_overrides": {},
"session_id": "sess-key", "api_mode": "chat_completions", **agent_fields,
})
return server._session_info(agent, {"session_key": "k7", "agent": agent})
def test_first_party_route_reports_fast(self) -> None:
info = self._info(model="gpt-5.4", provider="openai", base_url="https://api.openai.com/v1")
assert (info["service_tier"], info["fast"]) == ("priority", True)
def test_anthropic_route_reads_the_anthropic_base_url(self) -> None:
info = self._info(model="claude-opus-5", provider="anthropic", api_mode="anthropic_messages",
base_url="", _anthropic_base_url="https://api.anthropic.com")
assert info["fast"] is True
def test_local_model_keeps_the_tier_but_reports_no_fast(self) -> None:
info = self._info(model="Qwen3.8-27B-UD-Q4_K_M", provider="llamacpp", base_url="http://127.0.0.1:18434/v1")
assert (info["service_tier"], info["fast"]) == ("priority", False)
def test_fast_capable_model_behind_a_proxy_reports_no_fast(self) -> None:
info = self._info(model="gpt-5.4", provider="openrouter", base_url="https://openrouter.ai/api/v1")
assert info["fast"] is False
def test_pending_switch_is_judged_by_the_new_route(self) -> None:
"""Mid-turn the agent still holds the old base URL; the pending pick decides."""
agent = SimpleNamespace(
reasoning_config=None, service_tier="priority", request_overrides={}, session_id="sess-key",
api_mode="chat_completions", model="gpt-5.4", provider="openai", base_url="https://api.openai.com/v1")
session = {"session_key": "k8", "agent": agent, "pending_model_switch": {
"display_model": "Qwen3.8-27B-UD-Q4_K_M", "display_provider": "llamacpp"}}
assert server._session_info(agent, session)["fast"] is False
session["pending_model_switch"] = {"display_model": "claude-opus-5", "display_provider": "anthropic"}
assert server._session_info(agent, session)["fast"] is True

View File

@@ -2179,6 +2179,23 @@ def _live_session_identity(session: dict) -> tuple[str, str]:
return str(model), str(provider or "")
def _fast_tier_applies(agent, model: str, provider: str, *, route_known: bool) -> bool:
"""Whether a priority tier reaches this session's route. Every request builder asks the same gate, so a
profile-wide ``service_tier: fast`` sends nothing to a local server or a proxy, and the session must not
report Fast there either. ``route_known`` is False while a switch is pending: the agent's base URL still
belongs to the old route."""
from hermes_cli.models import resolve_fast_mode_overrides
base_url = None
if route_known and agent is not None:
if getattr(agent, "api_mode", None) == "anthropic_messages":
base_url = getattr(agent, "_anthropic_base_url", None)
base_url = base_url or getattr(agent, "base_url", None)
try:
return resolve_fast_mode_overrides(model, provider=provider or None, base_url=base_url) is not None
except Exception:
return False
def _session_info(agent, session: dict | None = None) -> dict:
if session is None:
session = next((c for c in _sessions.values() if c.get("agent") is agent), None)
@@ -2224,7 +2241,9 @@ def _session_info(agent, session: dict | None = None) -> dict:
"model": model,
"provider": pending_provider or provider,
"reasoning_effort": reasoning_effort, "reasoning_effort_wire": reasoning_effort_wire,
"service_tier": service_tier, "fast": service_tier == "priority",
"service_tier": service_tier,
"fast": service_tier == "priority" and _fast_tier_applies(agent, model, pending_provider or provider,
route_known=not pending_provider),
"yolo": yolo, "approval_mode": approval_mode,
"tools": dict(mirror.get("tools") or {}) if isinstance(mirror.get("tools"), dict) else {},
"skills": dict(mirror.get("skills") or {}) if isinstance(mirror.get("skills"), dict) else {},

View File

@@ -542,7 +542,7 @@ const StatusRulePane = memo(function StatusRulePane({
lastTurnEndedAt={status.lastTurnEndedAt}
liveSessionCount={ui.liveSessionCount}
model={ui.info?.model ?? ''}
modelFast={ui.info?.fast || ui.info?.service_tier === 'priority'}
modelFast={ui.info?.fast}
modelReasoningEffort={ui.info?.reasoning_effort}
modelReasoningEffortWire={ui.info?.reasoning_effort_wire}
notice={ui.notice}