Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
18 changes: 18 additions & 0 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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 "")
Expand Down
4 changes: 3 additions & 1 deletion hermes_cli/config_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading