mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
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:
parent
63dd651b3d
commit
9fa2906c18
7 changed files with 65 additions and 39 deletions
9
cli.py
9
cli.py
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue