fix(transport): use getattr for supports_prompt_cache_key on stale profiles
After a partial update (stash restore overwriting providers/base.py with
an older version), the NousProfile singleton was instantiated from a
ProviderProfile class that predates the supports_prompt_cache_key field
(added in f4fb23f3d). Accessing profile.supports_prompt_cache_key raised
AttributeError, crashing every API call with:
'NousProfile' object has no attribute 'supports_prompt_cache_key'
Use getattr(profile, 'supports_prompt_cache_key', False) so a stale
profile degrades to 'no prompt cache key' instead of crashing.
This commit is contained in:
@@ -740,7 +740,7 @@ class ChatCompletionsTransport(ProviderTransport):
|
||||
api_kwargs,
|
||||
messages=sanitized,
|
||||
tools=api_kwargs.get("tools"),
|
||||
supports_prompt_cache_key=bool(profile.supports_prompt_cache_key),
|
||||
supports_prompt_cache_key=bool(getattr(profile, "supports_prompt_cache_key", False)),
|
||||
session_id=params.get("session_id"),
|
||||
)
|
||||
|
||||
|
||||
+19
-17
@@ -86,8 +86,8 @@ def _reload_updated_runtime_modules() -> None:
|
||||
logger.debug("Could not refresh update runtime modules: %s", exc)
|
||||
|
||||
|
||||
def _run_config_check_fresh() -> tuple:
|
||||
"""Check config version using freshly-reloaded modules.
|
||||
def _reload_config_modules() -> None:
|
||||
"""Force-reload config modules from disk after git pull.
|
||||
|
||||
``hermes update`` runs in the PRE-pull Python process. After ``git pull``
|
||||
updates the source files on disk, modules already in ``sys.modules``
|
||||
@@ -96,9 +96,9 @@ def _run_config_check_fresh() -> tuple:
|
||||
``check_config_version()`` reports ``(33, 33)`` — "up to date" — even
|
||||
though the freshly-pulled code has v34 with a migration to run.
|
||||
|
||||
This function force-reloads ``hermes_cli.config`` and
|
||||
``hermes_cli.config_migrations`` from disk so the version check reads
|
||||
the UPDATED ``DEFAULT_CONFIG``. Returns ``(current_ver, latest_ver)``.
|
||||
This function force-reloads ``hermes_cli.config_defaults``,
|
||||
``hermes_cli.config``, and ``hermes_cli.config_migrations`` from disk
|
||||
so subsequent imports read the UPDATED code.
|
||||
"""
|
||||
import importlib
|
||||
|
||||
@@ -111,6 +111,14 @@ def _run_config_check_fresh() -> tuple:
|
||||
except Exception as exc:
|
||||
logger.debug("Could not reload %s for fresh config check: %s", mod_name, exc)
|
||||
|
||||
|
||||
def _run_config_check_fresh() -> tuple:
|
||||
"""Check config version using freshly-reloaded modules.
|
||||
|
||||
See ``_reload_config_modules`` for why this is necessary.
|
||||
Returns ``(current_ver, latest_ver)``.
|
||||
"""
|
||||
_reload_config_modules()
|
||||
from hermes_cli.config import check_config_version
|
||||
|
||||
return check_config_version()
|
||||
@@ -119,20 +127,10 @@ def _run_config_check_fresh() -> tuple:
|
||||
def _run_migrate_config_fresh(*, interactive: bool = False, quiet: bool = False) -> dict:
|
||||
"""Run config migration using freshly-reloaded modules.
|
||||
|
||||
See ``_run_config_check_fresh`` for why this is necessary.
|
||||
See ``_reload_config_modules`` for why this is necessary.
|
||||
Returns the migration results dict.
|
||||
"""
|
||||
import importlib
|
||||
|
||||
importlib.invalidate_caches()
|
||||
for mod_name in ("hermes_cli.config_defaults", "hermes_cli.config", "hermes_cli.config_migrations"):
|
||||
mod = sys.modules.get(mod_name)
|
||||
if mod is not None:
|
||||
try:
|
||||
importlib.reload(mod)
|
||||
except Exception as exc:
|
||||
logger.debug("Could not reload %s for fresh migration: %s", mod_name, exc)
|
||||
|
||||
_reload_config_modules()
|
||||
from hermes_cli.config import migrate_config
|
||||
|
||||
return migrate_config(interactive=interactive, quiet=quiet)
|
||||
@@ -4759,6 +4757,10 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
print()
|
||||
print("→ Checking configuration for new options...")
|
||||
|
||||
# Reload config modules BEFORE any config reads so get_missing_*,
|
||||
# check_config_version, and migrate_config all use the updated code.
|
||||
_reload_config_modules()
|
||||
|
||||
from hermes_cli.config import (
|
||||
get_missing_env_vars,
|
||||
get_missing_config_fields,
|
||||
|
||||
@@ -750,3 +750,29 @@ class TestPromptCacheKeyCapability:
|
||||
supports_prompt_cache_key=True,
|
||||
)
|
||||
assert kw1["prompt_cache_key"] != kw2["prompt_cache_key"]
|
||||
|
||||
def test_stale_profile_without_supports_prompt_cache_key_does_not_crash(self, transport):
|
||||
"""A ProviderProfile from a stale sys.modules cache (pre-#f4fb23f3d)
|
||||
won't have the ``supports_prompt_cache_key`` field. Accessing it via
|
||||
``profile.supports_prompt_cache_key`` raises AttributeError and crashes
|
||||
every API call. Use getattr with a False default so it degrades to
|
||||
"no prompt cache key" instead of crashing.
|
||||
|
||||
Regression: 'NousProfile' object has no attribute
|
||||
'supports_prompt_cache_key' (Aug 2026, after partial update).
|
||||
"""
|
||||
from providers.base import ProviderProfile
|
||||
|
||||
# Simulate a stale class that predates supports_prompt_cache_key
|
||||
# by creating a profile and deleting the attribute.
|
||||
profile = ProviderProfile(name="stale-provider")
|
||||
del profile.supports_prompt_cache_key
|
||||
|
||||
# Must not raise AttributeError — should fall back to False.
|
||||
kwargs = transport.build_kwargs(
|
||||
model="stale-model",
|
||||
messages=self._messages(),
|
||||
tools=self._tools(),
|
||||
provider_profile=profile,
|
||||
)
|
||||
assert "prompt_cache_key" not in kwargs
|
||||
|
||||
@@ -244,6 +244,8 @@ class TestCmdUpdateBranchFallback:
|
||||
), patch(
|
||||
"hermes_cli.config.get_missing_config_fields",
|
||||
return_value=[{"key": "new.option", "default": True}],
|
||||
), patch(
|
||||
"hermes_cli.update_cmd._reload_config_modules"
|
||||
), patch(
|
||||
"hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 2)
|
||||
), patch(
|
||||
@@ -285,6 +287,8 @@ class TestCmdUpdateMigrationPrompt:
|
||||
"hermes_cli.config.get_missing_env_vars", return_value=[]
|
||||
), patch(
|
||||
"hermes_cli.config.get_missing_config_fields", return_value=[]
|
||||
), patch(
|
||||
"hermes_cli.update_cmd._reload_config_modules"
|
||||
), patch(
|
||||
"hermes_cli.update_cmd._run_config_check_fresh", return_value=(5, 24)
|
||||
), patch(
|
||||
@@ -322,9 +326,11 @@ class TestCmdUpdateMigrationPrompt:
|
||||
), patch(
|
||||
"hermes_cli.config.get_missing_config_fields", return_value=cfg_items
|
||||
), patch(
|
||||
"hermes_cli.config.check_config_version", return_value=(1, 24)
|
||||
"hermes_cli.update_cmd._reload_config_modules"
|
||||
), patch(
|
||||
"hermes_cli.config.migrate_config",
|
||||
"hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 24)
|
||||
), patch(
|
||||
"hermes_cli.update_cmd._run_migrate_config_fresh",
|
||||
return_value={"env_added": [], "config_added": [], "warnings": []},
|
||||
), patch("hermes_cli.main.sys") as mock_sys:
|
||||
mock_sys.stdin.isatty.return_value = True
|
||||
@@ -356,27 +362,21 @@ class TestConfigVersionCheckUsesFreshModules:
|
||||
"""
|
||||
|
||||
def test_run_config_check_fresh_reloads_modules(self):
|
||||
"""_run_config_check_fresh must call importlib.reload on config modules."""
|
||||
import importlib
|
||||
import sys
|
||||
"""_run_config_check_fresh must call _reload_config_modules which
|
||||
force-reloads the config modules from disk.
|
||||
|
||||
Regression: config migration was silently skipped because
|
||||
sys.modules held the OLD hermes_cli.config with the OLD
|
||||
DEFAULT_CONFIG["_config_version"] after git pull.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
import hermes_cli.update_cmd as update_cmd
|
||||
|
||||
reloaded = []
|
||||
original_reload = importlib.reload
|
||||
|
||||
def tracking_reload(mod, *args, **kwargs):
|
||||
reloaded.append(mod.__name__)
|
||||
return original_reload(mod, *args, **kwargs)
|
||||
|
||||
with patch("importlib.reload", side_effect=tracking_reload):
|
||||
with patch.object(update_cmd, "_reload_config_modules") as mock_reload:
|
||||
update_cmd._run_config_check_fresh()
|
||||
|
||||
# The config modules that hold DEFAULT_CONFIG and MIGRATIONS must
|
||||
# have been reloaded — this is the fix for the stale-cache bug.
|
||||
assert "hermes_cli.config" in reloaded
|
||||
assert "hermes_cli.config_migrations" in reloaded
|
||||
mock_reload.assert_called_once()
|
||||
|
||||
|
||||
class TestCmdUpdateProfileSkillSync:
|
||||
|
||||
@@ -50,6 +50,7 @@ def _make_run_side_effect(
|
||||
class TestUpdateYesConfigMigration:
|
||||
"""--yes auto-answers the config-migration prompt and skips API-key prompts."""
|
||||
|
||||
@patch("hermes_cli.update_cmd._reload_config_modules")
|
||||
@patch("hermes_cli.update_cmd._run_migrate_config_fresh")
|
||||
@patch("hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 2))
|
||||
@patch("hermes_cli.config.get_missing_config_fields", return_value=[])
|
||||
@@ -64,6 +65,7 @@ class TestUpdateYesConfigMigration:
|
||||
_mock_missing_cfg,
|
||||
_mock_version,
|
||||
mock_migrate,
|
||||
_mock_reload,
|
||||
capsys,
|
||||
):
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
@@ -89,6 +91,7 @@ class TestUpdateYesConfigMigration:
|
||||
# The "Would you like to configure them now?" prompt text never appears.
|
||||
assert "Would you like to configure them now?" not in out
|
||||
|
||||
@patch("hermes_cli.update_cmd._reload_config_modules")
|
||||
@patch("hermes_cli.update_cmd._run_migrate_config_fresh")
|
||||
@patch("hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 2))
|
||||
@patch("hermes_cli.config.get_missing_config_fields", return_value=[])
|
||||
@@ -103,6 +106,7 @@ class TestUpdateYesConfigMigration:
|
||||
_mock_missing_cfg,
|
||||
_mock_version,
|
||||
mock_migrate,
|
||||
_mock_reload,
|
||||
capsys,
|
||||
):
|
||||
"""Regression guard: without --yes, the TTY prompt path still fires."""
|
||||
@@ -147,8 +151,9 @@ class TestUnicodeDecodeErrorInUpdatePrompts:
|
||||
the exception escape and crash `hermes update` mid-flight.
|
||||
"""
|
||||
|
||||
@patch("hermes_cli.config.migrate_config")
|
||||
@patch("hermes_cli.config.check_config_version", return_value=(1, 2))
|
||||
@patch("hermes_cli.update_cmd._reload_config_modules")
|
||||
@patch("hermes_cli.update_cmd._run_migrate_config_fresh")
|
||||
@patch("hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 2))
|
||||
@patch("hermes_cli.config.get_missing_config_fields", return_value=[])
|
||||
@patch("hermes_cli.config.get_missing_env_vars", return_value=["NEW_KEY"])
|
||||
@patch("shutil.which", return_value=None)
|
||||
@@ -161,6 +166,7 @@ class TestUnicodeDecodeErrorInUpdatePrompts:
|
||||
_mock_missing_cfg,
|
||||
_mock_version,
|
||||
mock_migrate,
|
||||
_mock_reload,
|
||||
capsys,
|
||||
):
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
|
||||
Reference in New Issue
Block a user