From 3fad25fd227696b6964144e26635e11b1df4a921 Mon Sep 17 00:00:00 2001 From: BB-light Date: Sat, 1 Aug 2026 13:41:31 +0530 Subject: [PATCH] fix(caching): honor prompt_caching.cache_ttl disable in config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting prompt_caching.cache_ttl to a falsy value (false, null, off, disabled, no, none) now fully disables prompt caching instead of being silently ignored. The disable propagates through anthropic_prompt_cache_policy() (early return when _cache_disabled flag is set) and restore_primary_runtime() (override after snapshot restore), so it survives /model switches and fallback re-derivation — the gap that caused #56105 to be reverted in #56126. Salvage of #33555 by @BB-light, with model-switch/fallback survival gap fixed on top. Co-authored-by: BB-light --- agent/agent_init.py | 18 ++++++++++ agent/agent_runtime_helpers.py | 18 ++++++++++ hermes_cli/config_defaults.py | 4 ++- scripts/release.py | 1 + tests/run_agent/test_run_agent.py | 60 +++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 1 deletion(-) diff --git a/agent/agent_init.py b/agent/agent_init.py index ef3199cb83749..f0fbffe17cdb0 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -834,10 +834,19 @@ def init_agent( agent._use_prompt_caching, agent._use_native_cache_layout = ( agent._anthropic_prompt_cache_policy() ) + agent._cache_disabled = False # Anthropic supports "5m" (default) and "1h" cache TTL tiers. Read from # config.yaml under prompt_caching.cache_ttl; unknown values keep "5m". # 1h tier costs 2x on write vs 1.25x for 5m, but amortizes across long # sessions with >5-minute pauses between turns (#14971). + # + # Setting cache_ttl to a falsy value (false / null / "off" / "disabled" / + # "no" / "none") disables prompt caching entirely. This is useful for + # OAuth subscription users where cache writes bill against "extra usage" + # or for third-party proxies that inject their own cache_control markers + # (#13477). The disable propagates through anthropic_prompt_cache_policy() + # and restore_primary_runtime() so it survives /model switches and + # fallback re-derivation (#33555). agent._cache_ttl = "5m" try: from hermes_cli.config import load_config_readonly as _load_pc_cfg @@ -846,6 +855,15 @@ def init_agent( _ttl = _pc_cfg.get("cache_ttl", "5m") if _ttl in {"5m", "1h"}: agent._cache_ttl = _ttl + elif ( + _ttl is False + or _ttl is None + or str(_ttl).lower() in ("off", "false", "disabled", "no", "none") + ): + agent._use_prompt_caching = False + agent._use_native_cache_layout = False + agent._cache_ttl = None + agent._cache_disabled = True except Exception: pass diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 5f72bedfb5628..a8a55fc57a87b 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -1475,6 +1475,12 @@ def restore_primary_runtime(agent) -> bool: "use_native_cache_layout", agent.api_mode == "anthropic_messages" and agent.provider == "anthropic", ) + # If the operator has disabled caching via config (cache_ttl is + # falsy → _cache_disabled flag is set), the disable must survive + # runtime snapshot restoration (#33555). + if getattr(agent, "_cache_disabled", False): + agent._use_prompt_caching = False + agent._use_native_cache_layout = False # ── Rebuild client for the primary provider ── if agent.provider == "moa": @@ -1866,7 +1872,19 @@ def anthropic_prompt_cache_policy( documented this for opencode-go Qwen; #24617 reports the same gateway contract for DeepSeek. Without markers these providers serve zero cache hits, re-billing the full prompt on every turn. + + If the operator has set ``prompt_caching.cache_ttl`` to a falsy value + (``false``, ``null``, ``"off"``, etc.) in config.yaml, prompt caching + is fully disabled — this early return ensures the disable survives + ``/model`` switches, fallback re-derivation, and runtime snapshot + restoration (#33555). We check ``"_cache_disabled"`` (set by + init_agent when the disable is detected) rather than ``_cache_ttl`` + directly, because ``_cache_ttl`` is not yet set when the policy runs + during the initial ``init_agent`` call. """ + if getattr(agent, "_cache_disabled", False): + return (False, False) + eff_provider = (provider if provider is not None else agent.provider) or "" eff_base_url = base_url if base_url is not None else (agent.base_url or "") eff_api_mode = api_mode if api_mode is not None else (agent.api_mode or "") diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 3ecf0b85d1e80..d57abef08f1c5 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -712,7 +712,9 @@ }, # Anthropic prompt caching (Claude via OpenRouter or native Anthropic API). - # cache_ttl must be "5m" or "1h" (Anthropic-supported tiers); other values are ignored. + # cache_ttl: "5m" or "1h" (Anthropic-supported tiers). Other non-falsy + # values are silently ignored. Falsy values (false, null, "off", + # "disabled", "no", "none") disable prompt caching entirely. "prompt_caching": { "cache_ttl": "5m", }, diff --git a/scripts/release.py b/scripts/release.py index c546e1b5efada..8820d33173a4a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -75,6 +75,7 @@ "embwl0x@users.noreply.github.com": "embwl0x", # PR #65105 salvage (gateway: preserve external supervisor ownership) "41409874+2751738943@users.noreply.github.com": "2751738943", # PR #54785 salvage (tui: post-turn completion ownership routing) "Burgunthy@users.noreply.github.com": "Burgunthy", # PR #20096 salvage (gateway: profile-based routing for inbound messages) + "BB-light@users.noreply.github.com": "BB-light", # PR #76015 salvage (caching: honor cache_ttl disable; #33555) "75556242+webtecnica@users.noreply.github.com": "webtecnica", # PR #63360 salvage (nous: restore inference-api base_url) "contato@webtecnica.com.br": "webtecnica", # PR #70888 salvage "skosarevivan@yandex.ru": "Epoxidex", # PR #29820 salvage (ollama: top-level reasoning_effort=none; #25758) diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 01225f241ec91..6e99fa59f580f 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -716,6 +716,66 @@ def test_prompt_caching_cache_ttl_defaults_without_config(self): ) assert a._cache_ttl == "5m" + @pytest.mark.parametrize( + "falsy_value", [False, None, "off", "false", "disabled", "no", "none"], + ) + def test_prompt_caching_disabled_by_falsy_cache_ttl(self, falsy_value): + """Falsy cache_ttl values should fully disable prompt caching.""" + with ( + patch("run_agent.get_tool_definitions", return_value=[]), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + patch( + "hermes_cli.config.load_config", + return_value={"prompt_caching": {"cache_ttl": falsy_value}}, + ), + patch( + "hermes_cli.config.load_config_readonly", + return_value={"prompt_caching": {"cache_ttl": falsy_value}}, + ), + ): + a = AIAgent( + api_key="test-k...7890", + model="anthropic/claude-sonnet-4-20250514", + base_url="https://openrouter.ai/api/v1", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + assert a._use_prompt_caching is False + assert a._use_native_cache_layout is False + assert a._cache_ttl is None + + def test_prompt_caching_disable_survives_policy_rederivation(self): + """The disable must survive anthropic_prompt_cache_policy() re-derivation + (called during /model switch and fallback activation).""" + with ( + patch("run_agent.get_tool_definitions", return_value=[]), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + patch( + "hermes_cli.config.load_config", + return_value={"prompt_caching": {"cache_ttl": False}}, + ), + patch( + "hermes_cli.config.load_config_readonly", + return_value={"prompt_caching": {"cache_ttl": False}}, + ), + ): + a = AIAgent( + api_key="test-k...7890", + model="anthropic/claude-sonnet-4-20250514", + base_url="https://openrouter.ai/api/v1", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + assert a._cache_ttl is None + # Re-run the policy (simulates /model switch or fallback) + should_cache, use_native = a._anthropic_prompt_cache_policy() + assert should_cache is False + assert use_native is False + assert a._use_prompt_caching is False def test_constructor_max_tokens_wins_over_config(self):