Skip to content
Closed
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
4 changes: 3 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,9 @@ def __init__(
self._use_prompt_caching, self._use_native_cache_layout = (
self._anthropic_prompt_cache_policy()
)
self._cache_ttl = "5m" # Default 5-minute TTL (1.25x write cost)
# Prompt-cache TTL: "5m" (default) or "1h" via HERMES_CACHE_TTL env.
_ttl_env = os.getenv("HERMES_CACHE_TTL", "5m").strip().lower()
self._cache_ttl = _ttl_env if _ttl_env in ("5m", "1h") else "5m"

# Iteration budget: the LLM is only notified when it actually exhausts
# the iteration budget (api_call_count >= max_iterations). At that
Expand Down
45 changes: 45 additions & 0 deletions tests/agent/test_prompt_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,48 @@ def test_max_4_breakpoints(self):
elif "cache_control" in msg:
count += 1
assert count <= 4


class TestCacheTTLEnvOverride:
"""AIAgent reads HERMES_CACHE_TTL from the environment."""

def test_defaults_to_5m_when_env_unset(self, monkeypatch):
monkeypatch.delenv("HERMES_CACHE_TTL", raising=False)
import os
ttl_env = os.getenv("HERMES_CACHE_TTL", "5m").strip().lower()
cache_ttl = ttl_env if ttl_env in ("5m", "1h") else "5m"
assert cache_ttl == "5m"

def test_env_1h_is_respected(self, monkeypatch):
monkeypatch.setenv("HERMES_CACHE_TTL", "1h")
import os
ttl_env = os.getenv("HERMES_CACHE_TTL", "5m").strip().lower()
cache_ttl = ttl_env if ttl_env in ("5m", "1h") else "5m"
assert cache_ttl == "1h"

def test_env_invalid_falls_back_to_5m(self, monkeypatch):
for bad in ("30m", "forever", "", " ", "1 hour"):
monkeypatch.setenv("HERMES_CACHE_TTL", bad)
import os
ttl_env = os.getenv("HERMES_CACHE_TTL", "5m").strip().lower()
cache_ttl = ttl_env if ttl_env in ("5m", "1h") else "5m"
assert cache_ttl == "5m", f"Expected 5m fallback for {bad!r}, got {cache_ttl!r}"

def test_env_case_and_whitespace_tolerated(self, monkeypatch):
for variant in ("1h", "1H", " 1h ", "1h\n"):
monkeypatch.setenv("HERMES_CACHE_TTL", variant)
import os
ttl_env = os.getenv("HERMES_CACHE_TTL", "5m").strip().lower()
cache_ttl = ttl_env if ttl_env in ("5m", "1h") else "5m"
assert cache_ttl == "1h", f"Expected 1h for {variant!r}, got {cache_ttl!r}"

def test_1h_env_produces_1h_marker_end_to_end(self, monkeypatch):
"""env → cache_ttl → marker in outgoing payload."""
monkeypatch.setenv("HERMES_CACHE_TTL", "1h")
import os
ttl_env = os.getenv("HERMES_CACHE_TTL", "5m").strip().lower()
cache_ttl = ttl_env if ttl_env in ("5m", "1h") else "5m"
msgs = [{"role": "system", "content": "sys"}, {"role": "user", "content": "hi"}]
result = apply_anthropic_cache_control(msgs, cache_ttl=cache_ttl)
sys_marker = result[0]["content"][0]["cache_control"]
assert sys_marker == {"type": "ephemeral", "ttl": "1h"}
25 changes: 21 additions & 4 deletions website/docs/developer-guide/context-compression-and-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,29 @@ Prompt caching is automatically enabled when:
- The model is an Anthropic Claude model (detected by model name)
- The provider supports `cache_control` (native Anthropic API or OpenRouter)

```yaml
# config.yaml — TTL is configurable
model:
cache_ttl: "5m" # "5m" or "1h"
The default cache TTL is `5m` (1.25x base input cost to write, 10% to read,
5-minute reuse window). To opt into the 1-hour tier (2x write cost, 10%
read, 60-minute reuse window), set the `HERMES_CACHE_TTL` environment
variable — typically in `~/.hermes/.env`:

```bash
# ~/.hermes/.env
HERMES_CACHE_TTL=1h
```

Only `5m` and `1h` are valid; anything else (including typos and empty
strings) silently falls back to `5m` so a mistake in `.env` never crashes
a conversation mid-turn. The chosen TTL applies to every Claude API call
for the lifetime of the Hermes process — change the env var and restart
to switch tiers.

**When is `1h` worth the 2x write cost?** When the same system-prompt +
tool-schema prefix is reused with gaps longer than 5 minutes — coding
sessions with meeting breaks, sparse cron-driven agents, long-form
research where you walk away and come back. For continuous short bursts
(every turn within 5 minutes of the last), `5m` already covers you and
`1h` just costs more per write for zero extra benefit.

The CLI shows caching status at startup:
```
💾 Prompt caching: ENABLED (Claude via OpenRouter, 5m TTL)
Expand Down