fix: restore base_url rstrip, extract should_clear_context_pin helper

Salvage follow-up for PR #68899:
- Restore .rstrip('/') on base_url in _swap_credential (both anthropic
  and OpenAI paths) to match every other assignment site. The route
  identity comparison still uses normalize_route_base_url which handles
  trailing slash correctly.
- Extract should_clear_context_pin() into hermes_cli/route_identity.py,
  consolidating 7 copy-pasted call sites across cli.py, gateway/run.py,
  gateway/slash_commands.py, and hermes_cli/model_switch.py into a
  single fail-closed helper.

C1 (anthropic path TLS re-application): pre-existing gap — the Anthropic
adapter (build_anthropic_client) has no TLS customization support at
all, so this is out of scope for this salvage.
This commit is contained in:
kshitijk4poor 2026-07-22 10:19:23 +05:30 committed by kshitij
parent 63dd651b3d
commit 9fa2906c18
7 changed files with 65 additions and 39 deletions

9
cli.py
View file

@ -8174,17 +8174,16 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
def _clear_persisted_context_for_model_switch(self, result) -> None:
"""Drop a global context pin when its configured owner changes."""
try:
from agent.agent_init import _context_route_mismatch
from hermes_cli.config import load_config_readonly
from hermes_cli.route_identity import should_clear_context_pin
config = load_config_readonly()
model_cfg = config.get("model", {}) if isinstance(config, dict) else {}
if not isinstance(model_cfg, dict) or "context_length" not in model_cfg:
return
configured_model = model_cfg.get("default") or model_cfg.get("model")
if (
configured_model and configured_model != result.new_model
) or _context_route_mismatch(
if should_clear_context_pin(
model_cfg.get("default") or model_cfg.get("model"),
result.new_model,
model_cfg.get("base_url"),
result.base_url,
model_cfg.get("provider"),

View file

@ -11959,9 +11959,11 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
_msg_config_ctx = None
if _msg_config_ctx is not None and isinstance(_msg_model_cfg, dict):
try:
from agent.agent_init import _context_route_mismatch
from hermes_cli.route_identity import should_clear_context_pin
if _context_route_mismatch(
if should_clear_context_pin(
None, # model match already checked above
None,
_msg_model_cfg.get("base_url"),
_msg_base_url,
_msg_model_cfg.get("provider"),
@ -12524,12 +12526,11 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if _hyg_config_context_length is not None:
try:
from agent.agent_init import _context_route_mismatch
from hermes_cli.route_identity import should_clear_context_pin
if (
_hyg_configured_model
and _hyg_model != _hyg_configured_model
) or _context_route_mismatch(
if should_clear_context_pin(
_hyg_configured_model,
_hyg_model,
_hyg_configured_base_url,
_hyg_base_url,
_hyg_configured_provider,
@ -13859,11 +13860,11 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if config_context_length is not None:
try:
from agent.agent_init import _context_route_mismatch
from hermes_cli.route_identity import should_clear_context_pin
if (
configured_model and model != configured_model
) or _context_route_mismatch(
if should_clear_context_pin(
configured_model,
model,
configured_base_url,
base_url,
configured_provider,

View file

@ -1730,16 +1730,12 @@ class GatewaySlashCommandsMixin:
_persist_model_cfg = {}
_persist_cfg["model"] = _persist_model_cfg
try:
from agent.agent_init import _context_route_mismatch
from hermes_cli.route_identity import should_clear_context_pin
_old_default = (
if should_clear_context_pin(
_persist_model_cfg.get("default")
or _persist_model_cfg.get("model")
)
if (
_old_default
and _old_default != result.new_model
) or _context_route_mismatch(
or _persist_model_cfg.get("model"),
result.new_model,
_persist_model_cfg.get("base_url"),
result.base_url,
_persist_model_cfg.get("provider"),
@ -2062,12 +2058,11 @@ class GatewaySlashCommandsMixin:
model_cfg = {}
cfg["model"] = model_cfg
try:
from agent.agent_init import _context_route_mismatch
from hermes_cli.route_identity import should_clear_context_pin
old_default = model_cfg.get("default") or model_cfg.get("model")
if (
old_default and old_default != result.new_model
) or _context_route_mismatch(
if should_clear_context_pin(
model_cfg.get("default") or model_cfg.get("model"),
result.new_model,
model_cfg.get("base_url"),
result.base_url,
model_cfg.get("provider"),

View file

@ -863,11 +863,11 @@ def resolve_display_context_length(
configured_model or configured_provider or configured_base_url
):
try:
from agent.agent_init import _context_route_mismatch
from hermes_cli.route_identity import should_clear_context_pin
if (
configured_model and configured_model != model
) or _context_route_mismatch(
if should_clear_context_pin(
configured_model,
model,
configured_base_url,
base_url,
configured_provider,

View file

@ -45,3 +45,32 @@ def normalize_route_base_url(base_url: Any) -> str:
if had_query_delimiter and not parsed.query:
normalized += "?"
return normalized
def should_clear_context_pin(
configured_model: Any,
active_model: Any,
configured_base_url: Any,
active_base_url: Any,
configured_provider: Any,
active_provider: Any,
) -> bool:
"""True when a configured ``model.context_length`` pin no longer matches its runtime route.
Fail-closed: any error during route comparison returns ``True`` (drop the pin)
so a stale window never silently inflates the compression threshold.
"""
configured_model = str(configured_model or "").strip()
if configured_model and configured_model != str(active_model or "").strip():
return True
try:
from agent.agent_init import _context_route_mismatch
return _context_route_mismatch(
configured_base_url,
active_base_url,
configured_provider,
active_provider,
)
except Exception:
return True

View file

@ -4770,18 +4770,18 @@ class AIAgent:
pass
self._anthropic_api_key = runtime_key
self._anthropic_base_url = runtime_base
self._anthropic_base_url = runtime_base.rstrip("/") if isinstance(runtime_base, str) else runtime_base
self._anthropic_client = build_anthropic_client(
runtime_key, runtime_base,
runtime_key, self._anthropic_base_url,
timeout=get_provider_request_timeout(self.provider, self.model),
)
self._is_anthropic_oauth = _is_oauth_token(runtime_key) if self.provider == "anthropic" else False
self.api_key = runtime_key
self.base_url = runtime_base
self.base_url = runtime_base.rstrip("/") if isinstance(runtime_base, str) else runtime_base
return
self.api_key = runtime_key
self.base_url = runtime_base
self.base_url = runtime_base.rstrip("/") if isinstance(runtime_base, str) else runtime_base
self._client_kwargs["api_key"] = self.api_key
self._client_kwargs["base_url"] = self.base_url
self._client_kwargs.pop("ssl_verify", None)

View file

@ -104,6 +104,8 @@ def test_credential_rotation_does_not_carry_global_headers_across_routes():
def test_credential_rotation_preserves_route_significant_trailing_segments():
"""Route identity comparison uses normalize_route_base_url, but the stored
base_url is stripped like every other assignment site (__init__, switch_model)."""
agent = SimpleNamespace(
api_mode="chat_completions",
provider="custom",
@ -127,5 +129,5 @@ def test_credential_rotation_preserves_route_significant_trailing_segments():
with patch("hermes_cli.config.load_config_readonly", return_value={}):
AIAgent._swap_credential(agent, entry)
assert agent.base_url == "https://b.example/v1//"
assert agent._client_kwargs["base_url"] == "https://b.example/v1//"
assert agent.base_url == "https://b.example/v1"
assert agent._client_kwargs["base_url"] == "https://b.example/v1"