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:
kshitij
2026-08-10 11:27:03 +05:30
committed by Teknium
parent f45a3fb2b0
commit e09ef9ebd8
5 changed files with 71 additions and 37 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+17 -17
View File
@@ -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:
+8 -2
View File
@@ -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(