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:
@@ -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
|
||||
|
||||
@@ -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 {},
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user