From 0080dd4110de8a76dabd1a5fe6cbaf6357a61326 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Tue, 21 Apr 2026 19:36:05 -0700
Subject: [PATCH 1/4] fix(kimi): map auto-discovered short model IDs to
API-accepted names (Closes #13758)
---
agent/prompt_builder.py | 8 +++++++-
hermes_cli/model_normalize.py | 17 +++++++++++++++++
tests/agent/test_prompt_builder.py | 18 ++++++++++++++++++
3 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py
index 2a210434949b..8e061f831b8e 100644
--- a/agent/prompt_builder.py
+++ b/agent/prompt_builder.py
@@ -350,7 +350,13 @@ def _strip_yaml_frontmatter(content: str) -> str:
),
"cli": (
"You are a CLI AI Agent. Try not to use markdown but simple text "
- "renderable inside a terminal."
+ "renderable inside a terminal. "
+ "File delivery: there is no attachment channel — the user reads your "
+ "response directly in their terminal. Do NOT emit MEDIA:/path tags "
+ "(those are only intercepted on messaging platforms like Telegram, "
+ "Discord, Slack, etc.; on the CLI they render as literal text). "
+ "When referring to a file you created or changed, just state its "
+ "absolute path in plain text; the user can open it from there."
),
"sms": (
"You are communicating via SMS. Keep responses concise and use plain text "
diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py
index 76dace065a3a..b1781162c5d5 100644
--- a/hermes_cli/model_normalize.py
+++ b/hermes_cli/model_normalize.py
@@ -408,6 +408,23 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
return bare
return _normalize_for_deepseek(bare)
+ # --- Kimi / Moonshot: map auto-discovered short IDs (k2p6) to API-accepted
+ # canonical names (kimi-k2.6). The /models endpoint returns internal
+ # short forms that fail at request time without this repair (issue #13758).
+ if provider in {"kimi-coding", "kimi-coding-cn", "moonshot"}:
+ bare = _strip_matching_provider_prefix(name, provider)
+ if "/" in bare:
+ bare = bare.split("/", 1)[1]
+ # k2p6 -> kimi-k2.6, k2p5 -> kimi-k2.5, etc.
+ _kimi_short_map = {
+ "k2p6": "kimi-k2.6",
+ "k2p5": "kimi-k2.5",
+ "k2p8": "kimi-k2.8",
+ }
+ if bare.lower() in _kimi_short_map:
+ return _kimi_short_map[bare.lower()]
+ return bare
+
# --- Direct providers: repair matching provider prefixes only ---
if provider in _MATCHING_PREFIX_STRIP_PROVIDERS:
return _strip_matching_provider_prefix(name, provider)
diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py
index 0962060313bb..11712b95192d 100644
--- a/tests/agent/test_prompt_builder.py
+++ b/tests/agent/test_prompt_builder.py
@@ -789,6 +789,24 @@ def test_platform_hints_known_platforms(self):
assert "cron" in PLATFORM_HINTS
assert "cli" in PLATFORM_HINTS
+ def test_cli_hint_does_not_suggest_media_tags(self):
+ # Regression: MEDIA:/path tags are intercepted only by messaging
+ # gateway platforms. On the CLI they render as literal text and
+ # confuse users. The CLI hint must steer the agent away from them.
+ cli_hint = PLATFORM_HINTS["cli"]
+ assert "MEDIA:" in cli_hint, (
+ "CLI hint should mention MEDIA: in order to tell the agent "
+ "NOT to use it (negative guidance)."
+ )
+ # Must contain explicit "don't" language near the MEDIA reference.
+ assert any(
+ marker in cli_hint.lower()
+ for marker in ("do not emit media", "not intercepted", "do not", "don't")
+ ), "CLI hint should explicitly discourage MEDIA: tags."
+ # Messaging hints should still advertise MEDIA: positively (sanity
+ # check that this test is calibrated correctly).
+ assert "include MEDIA:" in PLATFORM_HINTS["telegram"]
+
# =========================================================================
# Environment hints
From fd56da77243e4560fc7d110b9f3dafefce86dc1b Mon Sep 17 00:00:00 2001
From: pander <>
Date: Wed, 22 Apr 2026 11:40:55 +0800
Subject: [PATCH 2/4] fix(cli): deduplicate plugin toolsets against built-in
keys in hermes tools (Closes #13640)
---
hermes_cli/tools_config.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py
index 36b3c7f3f395..521e891e7e03 100644
--- a/hermes_cli/tools_config.py
+++ b/hermes_cli/tools_config.py
@@ -80,12 +80,20 @@ def _get_effective_configurable_toolsets():
Plugin toolsets are appended at the end so they appear after the
built-in toolsets in the TUI checklist.
+ Duplicate keys (plugin toolsets whose key already exists in
+ CONFIGURABLE_TOOLSETS) are skipped so the built-in row is kept —
+ plugin-added tools are extensions of that toolset, not a second
+ top-level row (issue #13640).
"""
result = list(CONFIGURABLE_TOOLSETS)
+ built_in_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
try:
from hermes_cli.plugins import discover_plugins, get_plugin_toolsets
discover_plugins() # idempotent — ensures plugins are loaded
- result.extend(get_plugin_toolsets())
+ plugin_toolsets = get_plugin_toolsets()
+ # Skip plugin toolsets whose key already exists in built-ins
+ plugin_toolsets = [(k, l, d) for k, l, d in plugin_toolsets if k not in built_in_keys]
+ result.extend(plugin_toolsets)
except Exception:
pass
return result
From 3274c91a2a3eb4f5548448cc06d9fbbf6f4f0ff4 Mon Sep 17 00:00:00 2001
From: pander <>
Date: Wed, 22 Apr 2026 11:48:26 +0800
Subject: [PATCH 3/4] fix(tui): fix status bar width fluctuation by capping
model name width (Closes #13610)
---
ui-tui/src/components/appChrome.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx
index 28f7b324e2f9..f7d2aee19832 100644
--- a/ui-tui/src/components/appChrome.tsx
+++ b/ui-tui/src/components/appChrome.tsx
@@ -131,7 +131,10 @@ export function StatusRule({
) : (
{status}
)}
- │ {model}
+ │
+
+ {model}
+
{ctxLabel ? │ {ctxLabel} : null}
{bar ? (
From 494cef6fde741322ecde4f91bde5bd556d73e318 Mon Sep 17 00:00:00 2001
From: pander <>
Date: Wed, 22 Apr 2026 11:57:08 +0800
Subject: [PATCH 4/4] fix(run_agent): read context_length from
providers/custom_providers for compression model
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When auxiliary.compression.model uses a custom provider with per-model
context_length configured in providers/local-kimi.models.kimi-code.context_length,
the compression feasibility check now looks up that value — mirroring the
main model's existing providers lookup in __init__.
Previously it only checked auxiliary.compression.context_length (explicit override)
and endpoint auto-detection, ignoring the per-model config in providers.
Closes #13807
---
run_agent.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/run_agent.py b/run_agent.py
index c5966a173706..8bb4b265ae2f 100644
--- a/run_agent.py
+++ b/run_agent.py
@@ -2178,11 +2178,58 @@ def _check_compression_model_feasibility(self) -> None:
aux_base_url = str(getattr(client, "base_url", ""))
aux_api_key = str(getattr(client, "api_key", ""))
+ # If no explicit override, look up context_length from
+ # providers / custom_providers for the auxiliary model — the
+ # same lookup that the main model uses in __init__.
+ _aux_ctx = getattr(self, "_aux_compression_context_length_config", None)
+ if _aux_ctx is None and aux_base_url:
+ try:
+ from hermes_cli.config import get_compatible_custom_providers
+ _custom_providers = get_compatible_custom_providers(_agent_cfg)
+ except Exception:
+ _custom_providers = _agent_cfg.get("custom_providers", [])
+ for _cp_entry in (_custom_providers or []):
+ if not isinstance(_cp_entry, dict):
+ continue
+ _cp_url = (_cp_entry.get("base_url") or "").rstrip("/")
+ if _cp_url and _cp_url == aux_base_url.rstrip("/"):
+ _cp_models = _cp_entry.get("models", {})
+ if isinstance(_cp_models, dict):
+ _cp_model_cfg = _cp_models.get(aux_model, {})
+ if isinstance(_cp_model_cfg, dict):
+ _cp_ctx = _cp_model_cfg.get("context_length")
+ if _cp_ctx is not None:
+ try:
+ _aux_ctx = int(_cp_ctx)
+ except (ValueError, TypeError):
+ pass
+ if _aux_ctx is not None:
+ break
+ # Also check the keyed providers schema (providers..models..context_length)
+ if _aux_ctx is None:
+ _providers = _agent_cfg.get("providers", {})
+ if isinstance(_providers, dict):
+ for _pk, _pentry in _providers.items():
+ if not isinstance(_pentry, dict):
+ continue
+ _purl = (_pentry.get("base_url") or "").rstrip("/")
+ if _purl and _purl == aux_base_url.rstrip("/"):
+ _pm = _pentry.get("models", {})
+ if isinstance(_pm, dict):
+ _pm_cfg = _pm.get(aux_model, {})
+ if isinstance(_pm_cfg, dict):
+ _pm_ctx = _pm_cfg.get("context_length")
+ if _pm_ctx is not None:
+ try:
+ _aux_ctx = int(_pm_ctx)
+ except (ValueError, TypeError):
+ pass
+
aux_context = get_model_context_length(
aux_model,
base_url=aux_base_url,
api_key=aux_api_key,
- config_context_length=getattr(self, "_aux_compression_context_length_config", None),
+ config_context_length=_aux_ctx,
)
# Hard floor: the auxiliary compression model must have at least