mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
tui: centralize RID-strip in format_model_for_display + apply to switch banner
Address review on PR #36998: the inline ri.<service>..<ns>. stripper in _get_status_bar_snapshot was a one-off heuristic that: * lived in cli.py with no shared call site, so the switch-confirmation banner ("✓ Model switched: ri.language-model-service..…") and the [Note: model was just switched from … to …] system-prompt nudge still printed the full opaque RID — exactly what the screenshot reported; * split on '..' and re-split on '.', which would mis-handle any RID whose namespace token isn't a single dotted segment. Refactor: * New module-level helper hermes_cli.model_switch.format_model_for_display matches on a startswith() allow-list (_OPAQUE_MODEL_PREFIXES) and returns the trailing slug. Falls through to the original string for every non-Palantir id, so HF paths (meta-llama/Llama-3.3-70B-Instruct), plain Claude/GPT names, .gguf paths, and aliased ids are untouched. Allow-list is extensible — add a prefix tuple entry for future proxies that wrap real names in a namespace (Bedrock ARNs are already covered by the slash-split fallback and have a different shape). * _get_status_bar_snapshot() now delegates to the shared helper after the reverse-alias miss (so configured aliases still win over the helper output). * cli.py::_handle_model_command — both confirmation-print blocks (~7720 and ~7975) now run result.new_model AND old_model through the formatter before they hit _cprint() and the _pending_model_switch_note text. * gateway/run.py model-switch handler (~10915) — same treatment for _pending_model_notes[_session_key] and the t('gateway.model.switched', model=…) confirmation line returned to the gateway client. The formatter is DISPLAY-ONLY. The session_model_overrides map, ModelSwitchResult.new_model, persistence to config.yaml, alias lookups, and every wire call still carry the full opaque RID — Palantir's API requires it. Verification: unit reproducer covers (a) all four Palantir model RIDs from this user's config stripped to the trailing slug, (b) plain model names (claude-4-7-opus-20260101, gpt-5.4, HF paths, empty string) passed through unchanged, (c) prefix-only edge preserved (no infinite-loop / empty-output regression). Refs: PR #36998 review feedback; screenshot showed model banner still printing the long RID after the original status-bar-only fix landed.
This commit is contained in:
parent
4e02320ed9
commit
8bec1540f0
3 changed files with 78 additions and 20 deletions
30
cli.py
30
cli.py
|
|
@ -4633,16 +4633,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
model_short = _reverse_alias_for_display(model_name)
|
||||
if model_short == model_name:
|
||||
model_short = model_name.split("/")[-1] if "/" in model_name else model_name
|
||||
# Strip Palantir RID prefixes that survived the slash split:
|
||||
# ``ri.language-model-service..language-model.anthropic-claude-4-7-opus``
|
||||
# → ``claude-4-7-opus``. The double-dot is Palantir's RID separator.
|
||||
if model_short.startswith("ri.") and ".." in model_short:
|
||||
_tail = model_short.split("..", 1)[1]
|
||||
# Drop the leading namespace token (``language-model.``).
|
||||
if "." in _tail:
|
||||
_tail = _tail.split(".", 1)[1]
|
||||
if _tail:
|
||||
model_short = _tail
|
||||
# Strip Palantir RID prefixes via the shared display formatter so
|
||||
# this site and ``ModelSwitchResult`` confirmation can't drift.
|
||||
from hermes_cli.model_switch import format_model_for_display
|
||||
model_short = format_model_for_display(model_short)
|
||||
if model_short.endswith(".gguf"):
|
||||
model_short = model_short[:-5]
|
||||
if len(model_short) > 26:
|
||||
|
|
@ -8058,14 +8052,18 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
)
|
||||
return
|
||||
|
||||
from hermes_cli.model_switch import format_model_for_display
|
||||
_display_old = format_model_for_display(old_model)
|
||||
_display_new = format_model_for_display(result.new_model)
|
||||
|
||||
self._pending_model_switch_note = (
|
||||
f"[Note: model was just switched from {old_model} to {result.new_model} "
|
||||
f"[Note: model was just switched from {_display_old} to {_display_new} "
|
||||
f"via {result.provider_label or result.target_provider}. "
|
||||
f"Adjust your self-identification accordingly.]"
|
||||
)
|
||||
|
||||
provider_label = result.provider_label or result.target_provider
|
||||
_cprint(f" ✓ Model switched: {result.new_model}")
|
||||
_cprint(f" ✓ Model switched: {_display_new}")
|
||||
_cprint(f" Provider: {provider_label}")
|
||||
|
||||
# Context: always resolve via the provider-aware chain so Codex OAuth,
|
||||
|
|
@ -8389,8 +8387,12 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
# Store a note to prepend to the next user message so the model
|
||||
# knows a switch occurred (avoids injecting system messages mid-history
|
||||
# which breaks providers and prompt caching).
|
||||
from hermes_cli.model_switch import format_model_for_display
|
||||
_display_old = format_model_for_display(old_model)
|
||||
_display_new = format_model_for_display(result.new_model)
|
||||
|
||||
self._pending_model_switch_note = (
|
||||
f"[Note: model was just switched from {old_model} to {result.new_model} "
|
||||
f"[Note: model was just switched from {_display_old} to {_display_new} "
|
||||
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.]"
|
||||
|
|
@ -8402,7 +8404,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
|
||||
# Display confirmation with full metadata
|
||||
provider_label = result.provider_label or result.target_provider
|
||||
_cprint(f" ✓ Model switched: {result.new_model}")
|
||||
_cprint(f" ✓ Model switched: {_display_new}")
|
||||
_cprint(f" Provider: {provider_label}")
|
||||
|
||||
# Context: always resolve via the provider-aware chain so Codex OAuth,
|
||||
|
|
|
|||
|
|
@ -1663,11 +1663,17 @@ class GatewaySlashCommandsMixin:
|
|||
"Failed to persist model switch to DB: %s", exc
|
||||
)
|
||||
|
||||
# Store model note + session override
|
||||
# Store model note + session override. Use display
|
||||
# form (strips opaque Palantir prefix) for the user-
|
||||
# visible note; session-override map still gets the
|
||||
# full opaque ID, which is what the wire needs.
|
||||
from hermes_cli.model_switch import format_model_for_display
|
||||
_display_cur = format_model_for_display(_cur_model)
|
||||
_display_new = format_model_for_display(result.new_model)
|
||||
if not hasattr(_self, "_pending_model_notes"):
|
||||
_self._pending_model_notes = {}
|
||||
_self._pending_model_notes[_session_key] = (
|
||||
f"[Note: model was just switched from {_cur_model} to {result.new_model} "
|
||||
f"[Note: model was just switched from {_display_cur} to {_display_new} "
|
||||
f"via {result.provider_label or result.target_provider}. "
|
||||
f"Adjust your self-identification accordingly.]"
|
||||
)
|
||||
|
|
@ -1743,9 +1749,11 @@ class GatewaySlashCommandsMixin:
|
|||
except Exception as e:
|
||||
logger.warning("Failed to persist model switch: %s", e)
|
||||
|
||||
# Build confirmation text
|
||||
# Build confirmation text. Use display form so opaque
|
||||
# Palantir IDs (ri.language-model-service..*) get
|
||||
# shortened to their trailing slug for the UI.
|
||||
plabel = result.provider_label or result.target_provider
|
||||
lines = [t("gateway.model.switched", model=result.new_model)]
|
||||
lines = [t("gateway.model.switched", model=format_model_for_display(result.new_model))]
|
||||
lines.append(t("gateway.model.provider_label", provider=plabel))
|
||||
mi = result.model_info
|
||||
from hermes_cli.model_switch import resolve_display_context_length
|
||||
|
|
@ -1939,10 +1947,13 @@ class GatewaySlashCommandsMixin:
|
|||
|
||||
# Store a note to prepend to the next user message so the model
|
||||
# knows about the switch (avoids system messages mid-history).
|
||||
# Display form strips opaque Palantir RID prefixes; the override
|
||||
# map below keeps the full ID for the wire.
|
||||
from hermes_cli.model_switch import format_model_for_display
|
||||
if not hasattr(self, "_pending_model_notes"):
|
||||
self._pending_model_notes = {}
|
||||
self._pending_model_notes[session_key] = (
|
||||
f"[Note: model was just switched from {current_model} to {result.new_model} "
|
||||
f"[Note: model was just switched from {format_model_for_display(current_model)} to {format_model_for_display(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.]"
|
||||
|
|
@ -2038,7 +2049,7 @@ class GatewaySlashCommandsMixin:
|
|||
|
||||
# Build confirmation message with full metadata
|
||||
provider_label = result.provider_label or result.target_provider
|
||||
lines = [t("gateway.model.switched", model=result.new_model)]
|
||||
lines = [t("gateway.model.switched", model=format_model_for_display(result.new_model))]
|
||||
lines.append(t("gateway.model.provider_label", provider=provider_label))
|
||||
|
||||
# Context: always resolve via the provider-aware chain so Codex OAuth,
|
||||
|
|
|
|||
|
|
@ -200,6 +200,51 @@ _NOUS_HERMES_NON_AGENTIC_RE = re.compile(
|
|||
)
|
||||
|
||||
|
||||
# Opaque internal model-ID display
|
||||
# ---------------------------------------------------------------------------
|
||||
# Some proxies (notably Palantir Foundry's LLM-proxy) identify models by
|
||||
# resource-instance IDs that are deeply nested, verbose, and pure noise to
|
||||
# read in CLI status output, e.g.:
|
||||
#
|
||||
# ri.language-model-service..language-model.anthropic-claude-4-7-opus
|
||||
#
|
||||
# The provider_label (e.g. "palantir-claude46") already carries the routing
|
||||
# context, so the only useful information left in the opaque ID is the
|
||||
# trailing slug. Strip the boilerplate prefix for *display* — never for
|
||||
# wire-side comparison, persistence, config writes, alias lookup, or
|
||||
# anything that round-trips back into the API.
|
||||
#
|
||||
# Match by substring on a known prefix so we never accidentally truncate
|
||||
# a legitimate model name that happens to contain dots.
|
||||
|
||||
_OPAQUE_MODEL_PREFIXES: tuple[str, ...] = (
|
||||
"ri.language-model-service..language-model.",
|
||||
)
|
||||
|
||||
|
||||
def format_model_for_display(model_name: str) -> str:
|
||||
"""Return a human-friendly form of *model_name* for CLI status output.
|
||||
|
||||
Strips known opaque proxy prefixes (Palantir Foundry's
|
||||
``ri.language-model-service..language-model.*``) and returns the
|
||||
trailing slug. Falls through to the original string for everything
|
||||
else, so real model IDs (``claude-4-7-opus-20260101``,
|
||||
``gpt-5-4``, ``meta-llama/Llama-3.3-70B-Instruct``) are untouched.
|
||||
|
||||
This is a DISPLAY-ONLY helper. Do NOT use the return value for any
|
||||
wire-side operation — the proxy expects the full opaque ID, and
|
||||
callers that compare or persist must keep the original.
|
||||
"""
|
||||
if not model_name:
|
||||
return model_name
|
||||
for prefix in _OPAQUE_MODEL_PREFIXES:
|
||||
if model_name.startswith(prefix):
|
||||
tail = model_name[len(prefix):]
|
||||
return tail if tail else model_name
|
||||
return model_name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def is_nous_hermes_non_agentic(model_name: str) -> bool:
|
||||
"""Return True if *model_name* is a real Nous Hermes 3/4 chat model.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue