mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat: add /model --once one-turn model override (#29914)
Adds --once to /model across CLI, TUI, and gateway: switch model for the next turn only, restoring the previous model in a finally block so success, exception, and interrupt all revert. Parsing extends parse_model_flags_detailed(); resolve_persist_behavior() treats --once as a persistence opt-out; --global + --once is rejected. Salvaged from PR #29923 (image-generation lane split to #59815 per review; conflict resolution against current main by the maintainers).
This commit is contained in:
parent
7ab95b4c9c
commit
3f84b7a163
9 changed files with 683 additions and 63 deletions
109
cli.py
109
cli.py
|
|
@ -24,6 +24,7 @@ except ModuleNotFoundError:
|
|||
pass
|
||||
|
||||
import logging
|
||||
import copy
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
|
@ -6163,7 +6164,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def _show_security_advisories(self):
|
||||
"""Show a startup banner if any unacked security advisories match.
|
||||
|
||||
|
|
@ -7835,6 +7836,67 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
self._restore_modal_input_snapshot()
|
||||
self._invalidate(min_interval=0.0)
|
||||
|
||||
def _snapshot_model_runtime(self) -> dict:
|
||||
"""Capture current CLI and agent model runtime for one-turn restore."""
|
||||
agent = getattr(self, "agent", None)
|
||||
return {
|
||||
"model": self.model,
|
||||
"provider": self.provider,
|
||||
"requested_provider": self.requested_provider,
|
||||
"_explicit_api_key": getattr(self, "_explicit_api_key", None),
|
||||
"_explicit_base_url": getattr(self, "_explicit_base_url", None),
|
||||
"api_key": self.api_key,
|
||||
"base_url": self.base_url,
|
||||
"api_mode": self.api_mode,
|
||||
"agent_primary_runtime": copy.deepcopy(
|
||||
getattr(agent, "_primary_runtime", None)
|
||||
) if agent is not None else None,
|
||||
}
|
||||
|
||||
def _restore_model_runtime_snapshot(self, snapshot: dict | None) -> None:
|
||||
"""Restore a model runtime captured before a one-turn override."""
|
||||
if not snapshot:
|
||||
return
|
||||
for key in (
|
||||
"model",
|
||||
"provider",
|
||||
"requested_provider",
|
||||
"_explicit_api_key",
|
||||
"_explicit_base_url",
|
||||
"api_key",
|
||||
"base_url",
|
||||
"api_mode",
|
||||
):
|
||||
if key in snapshot:
|
||||
setattr(self, key, snapshot.get(key))
|
||||
|
||||
agent = getattr(self, "agent", None)
|
||||
if agent is None:
|
||||
return
|
||||
|
||||
primary = snapshot.get("agent_primary_runtime")
|
||||
if primary and hasattr(agent, "_restore_primary_runtime"):
|
||||
try:
|
||||
agent._primary_runtime = copy.deepcopy(primary)
|
||||
agent._fallback_activated = True
|
||||
agent._rate_limited_until = 0
|
||||
if agent._restore_primary_runtime():
|
||||
return
|
||||
except Exception:
|
||||
logger.debug("CLI one-turn model restore via primary runtime failed", exc_info=True)
|
||||
|
||||
if hasattr(agent, "switch_model"):
|
||||
try:
|
||||
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", ""),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("CLI one-turn model restore failed: %s", exc)
|
||||
|
||||
@staticmethod
|
||||
def _compute_model_picker_viewport(
|
||||
selected: int,
|
||||
|
|
@ -8068,17 +8130,19 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
Supports:
|
||||
/model — show current model + usage hints
|
||||
/model <name> — switch model (persists by default)
|
||||
/model <name> --once — switch for the next turn only
|
||||
/model <name> --session — switch for this session only
|
||||
/model <name> --global — switch and persist (explicit)
|
||||
/model <name> --provider <provider> — switch provider + model
|
||||
/model --provider <provider> — switch to provider, auto-detect model
|
||||
|
||||
Persistence defaults to on (``model.persist_switch_by_default`` in
|
||||
config.yaml, default True). Use ``--session`` for a one-off switch.
|
||||
config.yaml, default True). Use ``--session`` for this CLI session or
|
||||
``--once`` for the next turn only.
|
||||
"""
|
||||
from hermes_cli.model_switch import (
|
||||
switch_model,
|
||||
parse_model_flags,
|
||||
parse_model_flags_detailed,
|
||||
resolve_persist_behavior,
|
||||
)
|
||||
from hermes_cli.providers import get_label
|
||||
|
|
@ -8087,19 +8151,25 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
parts = cmd_original.split(None, 1) # split off '/model'
|
||||
raw_args = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
# Parse --provider, --global, --session, and --refresh flags
|
||||
(
|
||||
model_input,
|
||||
explicit_provider,
|
||||
is_global_flag,
|
||||
force_refresh,
|
||||
is_session,
|
||||
) = parse_model_flags(raw_args)
|
||||
# Parse --provider, --global, --session, --once, and --refresh flags
|
||||
parsed_flags = parse_model_flags_detailed(raw_args)
|
||||
model_input = parsed_flags.model_input
|
||||
explicit_provider = parsed_flags.explicit_provider
|
||||
is_global_flag = parsed_flags.is_global
|
||||
force_refresh = parsed_flags.force_refresh
|
||||
is_session = parsed_flags.is_session
|
||||
one_turn = parsed_flags.is_once
|
||||
if is_global_flag and one_turn:
|
||||
_cprint(" ✗ /model --once cannot be combined with --global")
|
||||
return
|
||||
if one_turn and not model_input and not explicit_provider:
|
||||
_cprint(" ✗ /model --once requires a model or provider.")
|
||||
return
|
||||
# Resolve the effective persistence once: --session overrides the
|
||||
# config-gated default, --global forces persist, otherwise defer to
|
||||
# model.persist_switch_by_default (defaults to True so /model survives
|
||||
# across sessions).
|
||||
persist_global = resolve_persist_behavior(is_global_flag, is_session)
|
||||
persist_global = resolve_persist_behavior(is_global_flag, is_session, is_once=one_turn)
|
||||
|
||||
# --refresh: wipe the on-disk picker cache before building the
|
||||
# provider list. Forces a live re-fetch of every authed provider's
|
||||
|
|
@ -8148,6 +8218,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
_cprint(" No authenticated providers found.")
|
||||
_cprint("")
|
||||
_cprint(" /model <name> switch model (persists)")
|
||||
_cprint(" /model <name> --once switch for the next turn only")
|
||||
_cprint(" /model <name> --session switch for this session only")
|
||||
_cprint(" /model --provider <slug> switch provider")
|
||||
_cprint(" /model --refresh re-fetch live model lists")
|
||||
|
|
@ -8200,6 +8271,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
# Update requested_provider so _ensure_runtime_credentials() doesn't
|
||||
# overwrite the switch on the next turn (it re-resolves from this).
|
||||
old_model = self.model
|
||||
_one_turn_restore_snapshot = self._snapshot_model_runtime() if one_turn else None
|
||||
# Snapshot CLI-level fields before mutation so a failed in-place swap
|
||||
# rolls the whole CLI back to the old working model (#50163).
|
||||
_cli_snapshot = {
|
||||
|
|
@ -8254,8 +8326,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
self._pending_model_switch_note = (
|
||||
f"[Note: model was just switched from {old_model} to {result.new_model} "
|
||||
f"via {result.provider_label or result.target_provider}. "
|
||||
f"{'This override applies to the next turn only. ' if one_turn else ''}"
|
||||
f"Adjust your self-identification accordingly.]"
|
||||
)
|
||||
if one_turn:
|
||||
self._pending_one_turn_model_restore = _one_turn_restore_snapshot
|
||||
else:
|
||||
self._pending_one_turn_model_restore = None
|
||||
|
||||
# Display confirmation with full metadata
|
||||
provider_label = result.provider_label or result.target_provider
|
||||
|
|
@ -8305,6 +8382,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
save_config_value("model.base_url", result.base_url or None)
|
||||
save_config_value("model.api_mode", result.api_mode or None)
|
||||
_cprint(" Saved to config.yaml")
|
||||
elif one_turn:
|
||||
_cprint(" (next turn only — restores after one response)")
|
||||
else:
|
||||
_cprint(" (session only — add --global to persist)")
|
||||
|
||||
|
|
@ -11809,6 +11888,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
_persist_clean_user_message = (
|
||||
message if (_voice_prefix or agent_message != message) else None
|
||||
)
|
||||
_one_turn_model_restore = getattr(
|
||||
self, "_pending_one_turn_model_restore", None
|
||||
)
|
||||
self._pending_one_turn_model_restore = None
|
||||
try:
|
||||
result = self.agent.run_conversation(
|
||||
user_message=agent_message,
|
||||
|
|
@ -11838,6 +11921,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
"error": _summary,
|
||||
}
|
||||
finally:
|
||||
if _one_turn_model_restore:
|
||||
self._restore_model_runtime_snapshot(_one_turn_model_restore)
|
||||
# Surface any credit notices queued during the turn (cold-start
|
||||
# seed / per-turn capture) now that the response is done — printing
|
||||
# at this boundary paints cleanly above the prompt instead of being
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue