hermes-agent/hermes_cli/route_identity.py
kshitijk4poor 222ea2b6c9 refactor: fold simplify-code review findings
- extract _commit_registry/_note_refresh_failure shared by the background
  worker and foreground stage-4 (identical 4-step success + failure paths
  were duplicated); worker now commits under _models_dev_fetch_lock so a
  failing background refresh can never re-arm the backoff immediately
  after a successful force_refresh committed (unsynchronized-write race)
- add should_clear_context_pin_async to hermes_cli/route_identity.py
  (matching the get_model_context_length_async precedent) and use it at
  the 4 async gateway sites instead of inline asyncio.to_thread wraps;
  the sync _format_session_info site keeps the sync call (already
  off-loop via its callers' to_thread)
- test the background-refresh success path (the PR's primary new
  behavior): disk saved, mem cache swapped, backoff cleared, in_flight
  reset — mutation-checked
- replace the race-prone spin-wait on _models_dev_refresh_in_flight with
  a named-thread join in the backoff test
2026-07-29 17:13:48 +05:30

104 lines
3.2 KiB
Python

"""Fail-closed URL identity normalization for model/provider routes."""
from __future__ import annotations
from typing import Any
from urllib.parse import urlsplit, urlunsplit
def normalize_route_base_url(base_url: Any) -> str:
"""Canonicalize only proven-equivalent endpoint URL components."""
raw = str(base_url or "")
if not raw:
return ""
if any(ord(char) <= 0x20 for char in raw):
return raw
had_query_delimiter = "?" in raw.split("#", 1)[0]
try:
parsed = urlsplit(raw)
hostname = parsed.hostname
if not parsed.scheme or not hostname:
return raw
scheme = parsed.scheme.lower()
if "%" in hostname:
address, zone = hostname.split("%", 1)
host = f"{address.lower()}%{zone}"
else:
host = hostname.lower()
port = parsed.port
except (TypeError, ValueError):
return raw
route_host = parsed.netloc.rsplit("@", 1)[-1]
if route_host.startswith("[") or ":" in host:
host = f"[{host}]"
if port is not None and (scheme, port) not in {("http", 80), ("https", 443)}:
host = f"{host}:{port}"
if "@" in parsed.netloc:
host = f"{parsed.netloc.rsplit('@', 1)[0]}@{host}"
path = parsed.path
if path.endswith("/") and not had_query_delimiter:
path = path[:-1]
normalized = urlunsplit((scheme, host, path, parsed.query, ""))
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
async def should_clear_context_pin_async(
configured_model: Any,
active_model: Any,
configured_base_url: Any,
active_base_url: Any,
configured_provider: Any,
active_provider: Any,
) -> bool:
"""Async wrapper for ``should_clear_context_pin``.
Offloads the route comparison to a worker thread so async gateway
handlers never run it on the event loop — the resolution chain is
cache-only (``allow_network=False``) but can still do cold-start disk
I/O. Shares all logic with the sync version — no code duplication.
"""
import asyncio
return await asyncio.to_thread(
should_clear_context_pin,
configured_model,
active_model,
configured_base_url,
active_base_url,
configured_provider,
active_provider,
)