refactor(tui_gateway): AST-neutral layout compaction of group H modules
This commit is contained in:
@@ -87,8 +87,7 @@ def handle_spurious_eof(recovery_times: list[float], log_fn: object) -> bool:
|
||||
if len(recovery_times) > MAX_RECOVERIES_PER_MINUTE:
|
||||
log_fn( # type: ignore[operator]
|
||||
f"stdin spurious-EOF recovery rate exceeded "
|
||||
f"({len(recovery_times)}/min, cap {MAX_RECOVERIES_PER_MINUTE})"
|
||||
)
|
||||
f"({len(recovery_times)}/min, cap {MAX_RECOVERIES_PER_MINUTE})")
|
||||
return False
|
||||
log_fn(f"stdin spurious EOF (subprocess O_NONBLOCK flip), recovering: {diagnose_stdin_state()}") # type: ignore[operator]
|
||||
# Restore blocking mode on the shared description, and clear SO_RCVTIMEO too: a
|
||||
|
||||
@@ -22,8 +22,7 @@ def _wire_str(value):
|
||||
def _serialize_billing_error(exc) -> dict:
|
||||
"""Map a BillingError into the result.error envelope the TUI branches on."""
|
||||
from hermes_cli.nous_billing import (
|
||||
BillingRemoteSpendingRevoked, BillingScopeRequired, BillingSessionRevoked, BillingTransient,
|
||||
)
|
||||
BillingRemoteSpendingRevoked, BillingScopeRequired, BillingSessionRevoked, BillingTransient)
|
||||
kind = "error"
|
||||
if isinstance(exc, BillingRemoteSpendingRevoked):
|
||||
kind = "remote_spending_revoked"
|
||||
@@ -46,8 +45,7 @@ def _serialize_billing_error(exc) -> dict:
|
||||
# actor-aware copy + route recovery without re-parsing the message).
|
||||
"actor": getattr(exc, "actor", None),
|
||||
"code": getattr(exc, "code", None),
|
||||
"recovery": getattr(exc, "recovery", None),
|
||||
}
|
||||
"recovery": getattr(exc, "recovery", None)}
|
||||
|
||||
|
||||
def _serialize_payment_method(pm) -> dict | None:
|
||||
@@ -57,8 +55,7 @@ def _serialize_payment_method(pm) -> dict | None:
|
||||
return None
|
||||
if pm.kind == "card":
|
||||
return {
|
||||
"kind": "card", "brand": pm.brand, "last4": pm.last4, "wallet": pm.wallet,
|
||||
"resolved_via": pm.resolved_via,
|
||||
"kind": "card", "brand": pm.brand, "last4": pm.last4, "wallet": pm.wallet, "resolved_via": pm.resolved_via
|
||||
}
|
||||
if pm.kind == "link":
|
||||
return {"kind": "link", "email": pm.email, "resolved_via": pm.resolved_via}
|
||||
@@ -73,16 +70,14 @@ def _serialize_auto_reload(ar, format_money) -> dict | None:
|
||||
if ar.card.kind == "distinct":
|
||||
card_out = {
|
||||
"kind": "distinct", "payment_method_id": ar.card.payment_method_id,
|
||||
"brand": ar.card.brand, "last4": ar.card.last4,
|
||||
}
|
||||
"brand": ar.card.brand, "last4": ar.card.last4}
|
||||
else:
|
||||
card_out = {"kind": ar.card.kind}
|
||||
return {
|
||||
"enabled": ar.enabled, "threshold_usd": _wire_str(ar.threshold_usd),
|
||||
"threshold_display": format_money(ar.threshold_usd),
|
||||
"reload_to_usd": _wire_str(ar.reload_to_usd),
|
||||
"reload_to_display": format_money(ar.reload_to_usd), "card": card_out,
|
||||
}
|
||||
"reload_to_display": format_money(ar.reload_to_usd), "card": card_out}
|
||||
|
||||
|
||||
def _serialize_billing_state(state) -> dict:
|
||||
@@ -98,8 +93,7 @@ def _serialize_billing_state(state) -> dict:
|
||||
# None/False on older NAS payloads; resolved_via is the resolution
|
||||
# rung for rung-gated surfaces (/subscription confirm).
|
||||
"display": state.card.display,
|
||||
"resolved_via": state.card.resolved_via,
|
||||
}
|
||||
"resolved_via": state.card.resolved_via}
|
||||
monthly_cap = None
|
||||
if state.monthly_cap is not None:
|
||||
mc = state.monthly_cap
|
||||
@@ -107,8 +101,7 @@ def _serialize_billing_state(state) -> dict:
|
||||
"limit_usd": _wire_str(mc.limit_usd), "limit_display": format_money(mc.limit_usd),
|
||||
"spent_this_month_usd": _wire_str(mc.spent_this_month_usd),
|
||||
"spent_display": format_money(mc.spent_this_month_usd),
|
||||
"is_default_ceiling": mc.is_default_ceiling,
|
||||
}
|
||||
"is_default_ceiling": mc.is_default_ceiling}
|
||||
return {
|
||||
"ok": True,
|
||||
"logged_in": state.logged_in,
|
||||
@@ -133,8 +126,7 @@ def _serialize_billing_state(state) -> dict:
|
||||
"error": state.error,
|
||||
# Shared two-bar dollar usage model so /topup matches /usage and
|
||||
# /subscription from one fetch; fail-open.
|
||||
"usage": _usage_payload(state),
|
||||
}
|
||||
"usage": _usage_payload(state)}
|
||||
|
||||
|
||||
def _usage_payload(state) -> dict:
|
||||
@@ -156,8 +148,7 @@ def _serialize_usage_bar(bar) -> Optional[dict]:
|
||||
return {
|
||||
"kind": bar.kind, "remaining_display": _fmt_usd(bar.remaining_usd),
|
||||
"total_display": _fmt_usd(bar.total_usd), "spent_display": _fmt_usd(bar.spent_usd),
|
||||
"pct_used": bar.pct_used, "fill_fraction": bar.fill_fraction,
|
||||
}
|
||||
"pct_used": bar.pct_used, "fill_fraction": bar.fill_fraction}
|
||||
|
||||
|
||||
def _serialize_usage_model(model) -> dict:
|
||||
@@ -180,8 +171,7 @@ def _serialize_usage_model(model) -> dict:
|
||||
"total_spendable_display": _usd(model.total_spendable_usd),
|
||||
"has_topup": model.has_topup,
|
||||
"plan_bar": _serialize_usage_bar(model.plan_bar),
|
||||
"topup_bar": _serialize_usage_bar(model.topup_bar),
|
||||
}
|
||||
"topup_bar": _serialize_usage_bar(model.topup_bar)}
|
||||
|
||||
|
||||
def _serialize_subscription_state(state) -> dict:
|
||||
@@ -202,8 +192,7 @@ def _serialize_subscription_state(state) -> dict:
|
||||
"pending_downgrade_display": format_renews(c.pending_downgrade_at),
|
||||
"cancel_at_period_end": c.cancel_at_period_end,
|
||||
"cancellation_effective_at": c.cancellation_effective_at,
|
||||
"cancellation_effective_display": format_renews(c.cancellation_effective_at),
|
||||
}
|
||||
"cancellation_effective_display": format_renews(c.cancellation_effective_at)}
|
||||
# Selectable catalog for the in-terminal tier picker; price is pre-formatted
|
||||
# ($X / $X.YY) so the TUI renders it directly.
|
||||
tiers = [
|
||||
@@ -211,10 +200,8 @@ def _serialize_subscription_state(state) -> dict:
|
||||
"tier_id": t.tier_id, "name": t.name, "tier_order": t.tier_order,
|
||||
"dollars_per_month_display": format_money(t.dollars_per_month),
|
||||
"monthly_credits": _wire_str(t.monthly_credits), "is_current": t.is_current,
|
||||
"is_enabled": t.is_enabled,
|
||||
}
|
||||
for t in state.tiers
|
||||
]
|
||||
"is_enabled": t.is_enabled}
|
||||
for t in state.tiers]
|
||||
return {
|
||||
"ok": True,
|
||||
"logged_in": state.logged_in,
|
||||
@@ -230,8 +217,7 @@ def _serialize_subscription_state(state) -> dict:
|
||||
"error": state.error,
|
||||
# Shared two-bar usage model (account-info is the only source with
|
||||
# top-up dollars); fail-open → {available:false}; lazy when logged out.
|
||||
"usage": _usage_payload(state),
|
||||
}
|
||||
"usage": _usage_payload(state)}
|
||||
|
||||
|
||||
def _serialize_subscription_preview(p) -> dict:
|
||||
@@ -246,8 +232,7 @@ def _serialize_subscription_preview(p) -> dict:
|
||||
"target_tier_name": p.target_tier_name,
|
||||
"monthly_credits_delta": _wire_str(p.monthly_credits_delta),
|
||||
"amount_due_now_cents": p.amount_due_now_cents,
|
||||
"effective_at": p.effective_at,
|
||||
}
|
||||
"effective_at": p.effective_at}
|
||||
|
||||
|
||||
def register(server) -> None:
|
||||
|
||||
@@ -27,8 +27,7 @@ def resolve_skin() -> dict:
|
||||
"banner_logo": skin.banner_logo,
|
||||
"banner_hero": skin.banner_hero,
|
||||
"tool_prefix": skin.tool_prefix,
|
||||
"help_header": (skin.branding or {}).get("help_header", ""),
|
||||
}
|
||||
"help_header": (skin.branding or {}).get("help_header", "")}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@@ -117,8 +116,7 @@ def _pet_changed_payload() -> dict:
|
||||
"slug": pet.slug,
|
||||
"displayName": pet.display_name,
|
||||
"scale": scale,
|
||||
"spritesheetRevision": _pet_sheet_revision(pet.spritesheet),
|
||||
}
|
||||
"spritesheetRevision": _pet_sheet_revision(pet.spritesheet)}
|
||||
except Exception: # noqa: BLE001 - cosmetic, never break the watcher
|
||||
return {"enabled": False}
|
||||
|
||||
@@ -133,9 +131,7 @@ def _sessions_sig():
|
||||
cron runs (which never touch this gateway's transports) all move. Served sibling
|
||||
profile homes are probed too, else a routed profile's Bot Chat never refreshes."""
|
||||
return _newest_mtime_ns(
|
||||
root / name
|
||||
for root in (_watcher_home(), *_served_profile_homes)
|
||||
for name in ("state.db", "state.db-wal")
|
||||
root / name for root in (_watcher_home(), *_served_profile_homes) for name in ("state.db", "state.db-wal")
|
||||
)
|
||||
|
||||
|
||||
@@ -159,8 +155,7 @@ def _pairing_sig():
|
||||
with contextlib.suppress(OSError):
|
||||
# Only the ledgers: _rate_limits.json moves on every unauthorized DM.
|
||||
entries += [
|
||||
e for e in root.iterdir() if e.name.endswith(("-pending.json", "-approved.json"))
|
||||
]
|
||||
e for e in root.iterdir() if e.name.endswith(("-pending.json", "-approved.json"))]
|
||||
return _newest_mtime_ns(entries)
|
||||
|
||||
|
||||
@@ -195,8 +190,7 @@ _CHANGE_WATCHES: dict[str, tuple[float, Any, Any]] = {
|
||||
"platforms.changed": (2.0, _platforms_sig, lambda: {}),
|
||||
"pairing.changed": (2.0, _pairing_sig, lambda: {}),
|
||||
# 1s so a queued DM envelope reaches the Desktop's push-triggered drain fast.
|
||||
"bot_relay.outbox.pending": (1.0, _bot_relay_outbox_sig, lambda: {}),
|
||||
}
|
||||
"bot_relay.outbox.pending": (1.0, _bot_relay_outbox_sig, lambda: {})}
|
||||
|
||||
# state.db moves on every append of a streaming turn and gateway_state.json on
|
||||
# in-flight bookkeeping; the floor coalesces bursts to one broadcast per window,
|
||||
|
||||
@@ -100,5 +100,4 @@ def replay_stats() -> dict:
|
||||
return {
|
||||
"sessions": len(_replay_buffers),
|
||||
"events": sum(len(b) for b in _replay_buffers.values()),
|
||||
"max_per_session": _REPLAY_BUFFER_MAX,
|
||||
}
|
||||
"max_per_session": _REPLAY_BUFFER_MAX}
|
||||
|
||||
@@ -43,5 +43,4 @@ def summarize_server(name: str, cfg: dict) -> Dict[str, Any]:
|
||||
"auth": auth,
|
||||
"oauth_tokens_present": _oauth_tokens_present(name) if auth == "oauth" else None,
|
||||
"enabled": cfg.get("enabled", True) is not False,
|
||||
"tools": cfg.get("tools"),
|
||||
}
|
||||
"tools": cfg.get("tools")}
|
||||
|
||||
@@ -31,8 +31,7 @@ def _run_delivery(profile: str, tmp: str) -> subprocess.CompletedProcess:
|
||||
|
||||
return subprocess.run(
|
||||
local_delivery_command(profile, tmp), capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", timeout=600,
|
||||
)
|
||||
errors="replace", timeout=600)
|
||||
|
||||
|
||||
@method("bot_relay.roster.sync")
|
||||
@@ -143,8 +142,7 @@ def _(rid, params: dict, _root=_relay_root, _run=_run_delivery) -> dict:
|
||||
# compacts the over-threshold transcript first (no fresh session is
|
||||
# ever minted). Auth/quota/config classes never retry.
|
||||
from tools.bot_failure_reasons import (
|
||||
RETRY_NONE, classify_agent_error, retry_action,
|
||||
)
|
||||
RETRY_NONE, classify_agent_error, retry_action)
|
||||
|
||||
first_detail = (proc.stderr or proc.stdout or "").strip()[-500:]
|
||||
if retry_action(classify_agent_error(first_detail)) != RETRY_NONE:
|
||||
@@ -158,8 +156,7 @@ def _(rid, params: dict, _root=_relay_root, _run=_run_delivery) -> dict:
|
||||
detail = (proc.stderr or proc.stdout or "").strip()[-500:]
|
||||
return _err(
|
||||
rid, 5092, f"delivery turn failed: {detail or proc.returncode}",
|
||||
data={"reason": classify_agent_error(detail)},
|
||||
)
|
||||
data={"reason": classify_agent_error(detail)})
|
||||
return _ok(rid, {"reply": (proc.stdout or "").strip()})
|
||||
except subprocess.TimeoutExpired:
|
||||
return _err(rid, 5093, "delivery turn timed out")
|
||||
@@ -185,8 +182,7 @@ def _(rid, params: dict, _root=_relay_root) -> dict:
|
||||
|
||||
write_reply(
|
||||
_root(), envelope_id, reply=str(params.get("reply") or ""),
|
||||
error=str(params.get("error") or ""), reason=str(params.get("reason") or ""),
|
||||
)
|
||||
error=str(params.get("error") or ""), reason=str(params.get("reason") or ""))
|
||||
return _ok(rid, {"ok": True})
|
||||
except ValueError as e:
|
||||
return _err(rid, 4094, str(e))
|
||||
@@ -201,8 +197,7 @@ def register(server) -> None:
|
||||
server._LONG_HANDLERS = server._LONG_HANDLERS | methods_groups.LONG_HANDLERS
|
||||
for name in (
|
||||
"get_hosted_room_service", "_WORKER_UNAVAILABLE", "_profile_name", "_requested_profile",
|
||||
"_api_server_key", "_room_link_run_storage_durable",
|
||||
):
|
||||
"_api_server_key", "_room_link_run_storage_durable"):
|
||||
setattr(server, name, getattr(methods_groups, name))
|
||||
methods_groups.bind_server(server)
|
||||
methods_groups.register(server)
|
||||
|
||||
@@ -49,8 +49,7 @@ def _is_default_local_cdp(parsed) -> bool:
|
||||
parsed.scheme in {"http", "ws"}
|
||||
and parsed.hostname in {"127.0.0.1", "localhost"}
|
||||
and port == 9222
|
||||
and parsed.path in {"", "/", "/json", "/json/version"}
|
||||
)
|
||||
and parsed.path in {"", "/", "/json", "/json/version"})
|
||||
|
||||
|
||||
def _cdp_http_reachable(parsed, timeout: float = 2.0) -> bool:
|
||||
@@ -87,8 +86,7 @@ def _launch_failure_hints(port: int, system: str) -> list[str]:
|
||||
else [
|
||||
"No supported Chromium-family browser executable was found in this environment.",
|
||||
f"Install one or start a Chromium-family browser with --remote-debugging-port={port}, then retry /browser connect.",
|
||||
]
|
||||
)
|
||||
])
|
||||
return [
|
||||
*hint,
|
||||
"Browser not connected — start a Chromium-family browser with remote debugging and retry /browser connect",
|
||||
@@ -98,8 +96,7 @@ def _launch_failure_hints(port: int, system: str) -> list[str]:
|
||||
def _connect_local_default(port: int, system: str, announce) -> str | None:
|
||||
"""Discover (or launch) the default local debug browser → its CDP URL, or None after announcing failure."""
|
||||
from hermes_cli.browser_connect import (
|
||||
discover_local_cdp_url, find_free_debug_port, launch_chrome_debug, local_port_in_use,
|
||||
)
|
||||
discover_local_cdp_url, find_free_debug_port, launch_chrome_debug, local_port_in_use)
|
||||
|
||||
# Dual-stack discovery: when another app squats the IPv4 loopback on the debug
|
||||
# port, a browser bound there comes up on [::1] only. An IPv4-only probe misses
|
||||
@@ -114,8 +111,7 @@ def _connect_local_default(port: int, system: str, announce) -> str | None:
|
||||
announce(
|
||||
f"Port {port} is occupied by another application that isn't a CDP browser "
|
||||
"(an IDE debugger or dev server may be using it) — launching a debug browser "
|
||||
f"on port {launch_port} instead..."
|
||||
)
|
||||
f"on port {launch_port} instead...")
|
||||
else:
|
||||
announce("Chromium-family browser isn't running with remote debugging — attempting to launch...")
|
||||
launch = launch_chrome_debug(launch_port, system)
|
||||
|
||||
@@ -18,8 +18,7 @@ import hashlib
|
||||
import logging
|
||||
|
||||
from hermes_cli.dashboard_auth.ws_tickets import (
|
||||
INTERNAL_PROVIDER as _INTERNAL_PROVIDER, INTERNAL_USER_ID as _INTERNAL_USER_ID,
|
||||
)
|
||||
INTERNAL_PROVIDER as _INTERNAL_PROVIDER, INTERNAL_USER_ID as _INTERNAL_USER_ID)
|
||||
|
||||
from .method_ctx import HandlerRegistry, bind_module
|
||||
|
||||
@@ -65,8 +64,7 @@ def _broker_event_writer(transport: object, session_id: str):
|
||||
"jsonrpc": "2.0", "method": "event",
|
||||
"params": {
|
||||
"type": frame.get("method"), "session_id": session_id, "payload": frame.get("params"),
|
||||
},
|
||||
})
|
||||
}})
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"browser controller event write failed session=%s frame=%s", session_id, frame.get("method")
|
||||
@@ -80,8 +78,7 @@ def _broker_event_writer(transport: object, session_id: str):
|
||||
|
||||
def _controller_method(
|
||||
name: str, *, identity_message: str = _IDENTITY_REQUIRED, lookup_scope: bool = True,
|
||||
missing_scope_message: str = _NO_CONTROLLER, precheck=None,
|
||||
):
|
||||
missing_scope_message: str = _NO_CONTROLLER, precheck=None):
|
||||
"""Register a handler behind the shared fail-closed (4403) controller gates.
|
||||
|
||||
Order: ``precheck(rid, params)`` (may return an error envelope) → caller holds a
|
||||
@@ -113,8 +110,7 @@ def _controller_method(
|
||||
if lookup_scope:
|
||||
scope = broker.scope_for_session(
|
||||
session_id=session_id, principal_id=_principal_digest(identity),
|
||||
transport_family=_CLOUD_TRANSPORT_FAMILY,
|
||||
)
|
||||
transport_family=_CLOUD_TRANSPORT_FAMILY)
|
||||
if scope is None:
|
||||
return _err(rid, _ERR_FORBIDDEN, missing_scope_message)
|
||||
# Defense in depth: the broker's exact-scope ops already reject foreign
|
||||
@@ -143,8 +139,7 @@ def _register_precheck(rid, params: dict):
|
||||
@_controller_method(
|
||||
"browser.controller.register",
|
||||
identity_message="browser.controller.register requires an authenticated non-internal identity",
|
||||
lookup_scope=False, precheck=_register_precheck,
|
||||
)
|
||||
lookup_scope=False, precheck=_register_precheck)
|
||||
def _(rid, params: dict, transport, identity, session_id, broker, _scope, session) -> dict:
|
||||
"""Attach this connection as the browser controller for one session.
|
||||
|
||||
@@ -167,8 +162,7 @@ def _(rid, params: dict, transport, identity, session_id, broker, _scope, sessio
|
||||
scope = browser_control_broker.ControllerScope(
|
||||
principal_id=_principal_digest(identity), profile_id=profile_id, session_id=session_id,
|
||||
controller_id=controller_id, browser_profile_id=browser_profile_id,
|
||||
transport_family=_CLOUD_TRANSPORT_FAMILY, capabilities=capabilities,
|
||||
)
|
||||
transport_family=_CLOUD_TRANSPORT_FAMILY, capabilities=capabilities)
|
||||
broker.attach(scope, _broker_event_writer(transport, session_id), owner=transport)
|
||||
return _ok(rid, {
|
||||
"scope": {
|
||||
@@ -178,9 +172,7 @@ def _(rid, params: dict, transport, identity, session_id, broker, _scope, sessio
|
||||
"controller_id": scope.controller_id,
|
||||
"browser_profile_id": scope.browser_profile_id,
|
||||
"transport_family": scope.transport_family,
|
||||
"capabilities": sorted(scope.capabilities),
|
||||
}
|
||||
})
|
||||
"capabilities": sorted(scope.capabilities)}})
|
||||
|
||||
|
||||
@_controller_method("browser.controller.result")
|
||||
@@ -195,8 +187,7 @@ def _(rid, params: dict, _transport, _identity, _session_id, broker, scope, _ses
|
||||
return _err(rid, _ERR_FORBIDDEN, "command_id required")
|
||||
ok = params.get("ok") is True
|
||||
accepted = broker.complete(
|
||||
command_id, scope=scope, ok=ok, result=params.get("result") if ok else params.get("error")
|
||||
)
|
||||
command_id, scope=scope, ok=ok, result=params.get("result") if ok else params.get("error"))
|
||||
return _ok(rid, {"accepted": accepted})
|
||||
|
||||
|
||||
|
||||
@@ -50,8 +50,7 @@ def _restore_agent_model_runtime(agent, snapshot: dict | None) -> None:
|
||||
agent.switch_model(
|
||||
new_model=snapshot.get("model", ""), new_provider=snapshot.get("provider", ""),
|
||||
api_key=snapshot.get("api_key", ""), base_url=snapshot.get("base_url", ""),
|
||||
api_mode=snapshot.get("api_mode", ""), capabilities=snapshot.get("capabilities"),
|
||||
)
|
||||
api_mode=snapshot.get("api_mode", ""), capabilities=snapshot.get("capabilities"))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -84,8 +83,7 @@ def _restart_completed_failed_agent_build(sid: str, session: dict, failed_ready:
|
||||
with build_lock:
|
||||
if (
|
||||
session.get("agent") is not None or session.get("agent_error") is None
|
||||
or session.get("agent_ready") is not failed_ready or not failed_ready.is_set()
|
||||
):
|
||||
or session.get("agent_ready") is not failed_ready or not failed_ready.is_set()):
|
||||
return False
|
||||
model_override = session.get("model_override")
|
||||
resume_overrides = session.get("resume_runtime_overrides")
|
||||
@@ -108,8 +106,7 @@ def _restart_completed_failed_agent_build(sid: str, session: dict, failed_ready:
|
||||
def _switch_request(raw_input: str, parsed_flags, persist_override) -> tuple[str, str, bool, bool]:
|
||||
"""Normalize /model flags → (model_input, explicit_provider, one_turn, persist_global); raises on conflict."""
|
||||
from hermes_cli.model_switch import (
|
||||
MODEL_SWITCH_ERR_ONCE_WITH_GLOBAL, MODEL_SWITCH_ERROR_TEXT, parse_model_switch_args,
|
||||
resolve_persist_behavior,
|
||||
MODEL_SWITCH_ERR_ONCE_WITH_GLOBAL, MODEL_SWITCH_ERROR_TEXT, parse_model_switch_args, resolve_persist_behavior
|
||||
)
|
||||
|
||||
if parsed_flags is None:
|
||||
@@ -126,8 +123,7 @@ def _switch_request(raw_input: str, parsed_flags, persist_override) -> tuple[str
|
||||
raise ValueError(MODEL_SWITCH_ERROR_TEXT[MODEL_SWITCH_ERR_ONCE_WITH_GLOBAL])
|
||||
if persist_override is None:
|
||||
persist_override = resolve_persist_behavior(
|
||||
is_global_flag, is_session, is_once=one_turn, explicit_provider=explicit_provider
|
||||
)
|
||||
is_global_flag, is_session, is_once=one_turn, explicit_provider=explicit_provider)
|
||||
if not model_input:
|
||||
raise ValueError("model value required")
|
||||
return model_input, explicit_provider, one_turn, persist_override
|
||||
@@ -178,8 +174,7 @@ def _merge_preflight_warning(result, agent, session: dict, cfg, custom_provs) ->
|
||||
cfg_ctx = int(mc["context_length"])
|
||||
merge_preflight_compression_warning(
|
||||
result, agent=agent, messages=list(session.get("history", [])),
|
||||
custom_providers=custom_provs, config_context_length=cfg_ctx,
|
||||
)
|
||||
custom_providers=custom_provs, config_context_length=cfg_ctx)
|
||||
except Exception as exc:
|
||||
logger.debug("preflight-compression switch warning failed: %s", exc)
|
||||
|
||||
@@ -191,8 +186,7 @@ def _expensive_model_confirm(result, current_base_url: str, current_api_key) ->
|
||||
|
||||
warning = combined_selection_warning(
|
||||
result.new_model, provider=result.target_provider, base_url=result.base_url or current_base_url,
|
||||
api_key=result.api_key or current_api_key, model_info=result.model_info,
|
||||
)
|
||||
api_key=result.api_key or current_api_key, model_info=result.model_info)
|
||||
except Exception:
|
||||
warning = None
|
||||
if warning is None:
|
||||
@@ -211,8 +205,7 @@ def _commit_agent_switch(sid: str, session: dict, agent, result, current_model:
|
||||
agent.switch_model(
|
||||
new_model=result.new_model, new_provider=result.target_provider, api_key=result.api_key,
|
||||
base_url=result.base_url, api_mode=result.api_mode,
|
||||
capabilities=getattr(result, "runtime_capabilities", None),
|
||||
)
|
||||
capabilities=getattr(result, "runtime_capabilities", None))
|
||||
except Exception as exc:
|
||||
# The in-place swap rolled the agent back and re-raised. Abort the whole
|
||||
# commit (worker restart, persist, marker, override, config write) or the
|
||||
@@ -236,19 +229,16 @@ def _commit_agent_switch(sid: str, session: dict, agent, result, current_model:
|
||||
def _apply_model_switch(
|
||||
sid: str, session: dict, raw_input: str, *, confirm_expensive_model: bool = False,
|
||||
pin_session_override: bool = True, parsed_flags: Any | None = None,
|
||||
persist_override: bool | None = None,
|
||||
) -> dict:
|
||||
persist_override: bool | None = None) -> dict:
|
||||
from hermes_cli.model_switch import switch_model
|
||||
|
||||
model_input, explicit_provider, one_turn, persist_global = _switch_request(
|
||||
raw_input, parsed_flags, persist_override
|
||||
)
|
||||
raw_input, parsed_flags, persist_override)
|
||||
agent = session.get("agent")
|
||||
if one_turn and not agent:
|
||||
raise ValueError("/model --once requires a live session")
|
||||
current_provider, current_model, current_base_url, current_api_key = _current_model_runtime(
|
||||
agent, explicit_provider
|
||||
)
|
||||
agent, explicit_provider)
|
||||
# User-defined providers let switch_model resolve named custom endpoints
|
||||
# (e.g. "ollama-launch") and validate against saved model lists.
|
||||
user_provs, custom_provs, cfg = _provider_context()
|
||||
@@ -275,16 +265,14 @@ def _apply_model_switch(
|
||||
if pin_session_override and isinstance(session, dict) and not one_turn:
|
||||
session["model_override"] = {
|
||||
"model": result.new_model, "provider": result.target_provider,
|
||||
"base_url": result.base_url, "api_key": result.api_key, "api_mode": result.api_mode,
|
||||
}
|
||||
"base_url": result.base_url, "api_key": result.api_key, "api_mode": result.api_mode}
|
||||
if persist_global:
|
||||
_persist_model_switch(result)
|
||||
return {
|
||||
"value": result.new_model,
|
||||
"warning": result.warning_message or "",
|
||||
"confirm_required": False,
|
||||
"scope": "once" if one_turn else ("global" if persist_global else "session"),
|
||||
}
|
||||
"scope": "once" if one_turn else ("global" if persist_global else "session")}
|
||||
|
||||
|
||||
def _sync_bot_capabilities(sid: str, session: dict) -> None:
|
||||
|
||||
Reference in New Issue
Block a user