Skip to content

feat(llm,agent): opt-in 1h prompt-cache TTL for Claude (cache_ttl) - #246

Open
collen-steffen wants to merge 1 commit into
wippyai:masterfrom
collen-steffen:feat/claude-cache-ttl
Open

collen-steffen wants to merge 1 commit into
wippyai:masterfrom
collen-steffen:feat/claude-cache-ttl

Conversation

@collen-steffen

Copy link
Copy Markdown
Contributor

Problem

The Claude driver always marks cache breakpoints {type = "ephemeral"}, which uses Claude's 5-minute default. The cache lifetime runs from the start of the request that wrote or read it, so the time spent generating counts against it. An agent step that thinks or runs a tool for a few minutes therefore loses its cache. The next request then re-sends the whole prefix uncached.

Measured on a Fortress document-drafting agent on DEV (Claude Sonnet 5.5, ~300k-token context, one delegated dataflow run):

step duration next request cache_read_tokens
16 (plan) 6m13s 0
17 (first edit) 5m41s 0
18 2m21s 305,000 (hit)

Each miss re-prefills about 300k tokens. That is slower (time to first token) and about 10× the input cost of a cache read.

Change

  • llm / Claude driver: new options.cache_ttl, either "5m" or "1h". With "1h", every cache_control block the mapper writes gets ttl = "1h". Every marker shares one TTL, so the API's rule that longer TTLs come first is always met. Any other value returns invalid_request. When the option is omitted, the payload is byte-for-byte what it is today.
    • mapper.map_messages(messages, { cache_ttl = ... }), plus helpers mapper.cache_control() and mapper.valid_cache_ttl().
    • generate.lua and structured_output.lua pass the option through.
  • agent: an agent definition can set cache_ttl: 1h. It flows through discovery and the compiler into each step's LLM options, the same way thinking_effort does. Other drivers already ignore unknown options (map_options is an allow-list).
  • docs: the llm and agent READMEs gain the new option.

The 1-hour TTL is GA on the Claude API with no beta header. Writes cost 2× the base input price instead of 1.25×, and reads cost the same. It is opt-in per agent because it only pays off when consecutive requests start more than about 5 minutes apart.

Tests

  • src/llm/test/claude/mapper_test.lua: with "1h", the TTL lands on the system and message markers; without it, there is no TTL field; validation accepts only nil, "5m" and "1h".
  • src/agent/test/agent_test.lua: cache_ttl reaches the LLM options when configured and is absent otherwise.

I could not run the suite locally (no verified runtime on this machine), so I'm relying on CI here.

Out of scope: Bedrock (cachePoint) is unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_0139v7159kcEYoZk3JYhKwtK

Claude prompt-cache markers were always sent as {type="ephemeral"}, the
5-minute default. An agent step that thinks or runs a tool for longer than
5 minutes then re-sends its whole prefix uncached on the next request.

Measured on a document-drafting agent (Claude Sonnet 5.5, ~300k-token
context): two steps of 6m13s and 5m41s were each followed by a request with
cache_read_tokens = 0, while a 2m21s step was followed by a full cache hit.

- llm (Claude driver): options.cache_ttl "5m" | "1h". "1h" marks every
  cache_control block with ttl="1h"; any other value is invalid_request.
  Omitted keeps today's payload byte-for-byte.
- agent: an agent definition may set cache_ttl; it passes through discovery
  and the compiler into the step's LLM options, like thinking_effort.
- tests: mapper (ttl applied / default unchanged / validation) and agent
  (passed only when configured).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139v7159kcEYoZk3JYhKwtK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant