Release notes / PR description for
v0.3.1. This file is the source of truth — paste the body below into GitHub Release / PR description forms as needed. The CHANGELOG entry is the canonical compressed version.
The realistic agent preset that ships with the bench harness had a
cache key regression: role was randomized per call, silently
invalidating Anthropic prompt cache every time. The optimized arm was
paying the 1.25× cache-write surcharge and getting zero cache-reads
back — directly contradicting the purpose of the bench. After fixing
(3-line patch in prompt_factory.py), the optimized arm is 90%
cheaper per call at n=30 with 89.2% mean cache hit rate.
The bench harness sends the "stable" prefix (system prompt + tool schema
- agent role) as an Anthropic
system[]content block withcache_control: {type: "ephemeral"}. Anthropic's prompt cache keys on exact-match hashing of that prefix.
The realistic agent preset pinned system and tools to constants,
but generate_one did this for the role section:
role=random.choice(ROLE_PROMPTS) if random.random() < 0.6 else "",Six candidate role strings (weather-agent, code-assistant,
data-analyst, support-bot, translator, ""...) rotating across
calls meant the cache key rotated too. Anthropic correctly reported
cache_creation_input_tokens > 0 and cache_read_input_tokens = 0
every single call — the standard "I have never seen this prefix before"
signal.
Cost impact (per Anthropic Sonnet 4.6 pricing):
- A normal input token: $3.00 / M
- A cache-read token: $0.30 / M (90% discount)
- A cache-write token: $3.75 / M (25% surcharge)
So every "optimized" call was paying +$0.0026 more than the baseline call (the 0.25× surcharge on 3,476 system tokens) and getting nothing back. Optimized was more expensive than baseline per call — exactly the opposite of what the bench was supposed to demonstrate.
# contextops_bench/prompt_factory.py
AGENT_PRESETS: dict[str, dict[str, str]] = {
"realistic": {
"system": REALISTIC_AGENT_SYSTEM,
"tools": REALISTIC_AGENT_TOOLS,
+ # Identity is part of the agent definition — must be constant for
+ # cache to be hit across calls. Randomizing role rotates the cache
+ # key and silently turns every call into a cold cache_write.
+ "role": "code-assistant",
},
}Plus 3 lines in generate_one / generate_many to thread a
fixed_role parameter through (mirroring fixed_system, fixed_tools,
fixed_model). One line in __main__._execute to read role from
the preset and log it at startup — so any future regression in
preset-pinning is immediately visible in the bench output.
Three layers of "it works on my machine" compounded:
-
The bench harness itself wasn't running against Anthropic until
direct_zen/direct_anthropicproviders landed. Tests ran againstEchoClient(offline stub that simulates cache behavior with a prefix-match algorithm that did tolerate per-call role changes in degraded mode). Sopytestwas happy whileclaude-sonnet-4-6was unhappy. -
OpenRouter's OpenAI-compatible adapter drops the
cache_controlmarker during OpenAI → Anthropic translation, so cache reads always show 0 even with correct setup. The bench going through OpenRouter first gave a false signal that "cache control doesn't work". Switching to OpenCode-ZEN (which passes Anthropic-nativecache_controlthrough unchanged) was what made thecache_creationsignal visible. -
The 6/30 vs 0/5 ratio across two n runs. A first n=30 run with the bug still in place got 6/30 cache hits — looking vaguely plausible, masking the systematic failure. The randomness of the
rolerotation meant some prompts shared role with a previous call, leading to a "bursty, account-size-limited cache pool" hypothesis (suggesting cache TTL constraints, account size, etc.) that was actually a red herring.
| Optimized | Baseline | Δ | |
|---|---|---|---|
| Mean prompt tokens | 398 | 3,750 | −3,352 |
| Mean cached tokens | 3,364 | 0 | +3,364 |
| Cache hit rate | 89.2% | 0.0% | +89.2pp |
| Cost / call | $0.00107 | $0.01062 | −$0.00955 |
| Total run cost | $0.032 | $0.319 | −$0.287 |
Across a single 60-call A/B run, the optimized arm ran for $0.032 vs $0.319 baseline — saved $0.287 per run. Project that to an agent making 10K similar calls/day: ~$95/day, ~$2,860/month saved on a single Sonnet-4.6 instance.
(A few transport errors during the run — 1 optimized, 2 baseline — unrelated to caching. The comparison stands.)
Nothing. The fix is fully internal. If you were running the bench
harness against direct_zen or direct_anthropic with the realistic
preset, your next pip install --upgrade contextops-tool will give you
correct cache measurements.
If you were running with --fixed-system and --fixed-tools only (no
--preset-agent), the bench was always using randomized role — so your
prior measurements would have been symptomatic too. Re-run after
upgrading to confirm.
- The pinned role value is
"code-assistant"— chosen to be semantically consistent with theREALISTIC_AGENT_SYSTEMtext ("You are Atlas, a senior software engineering assistant"). - If you need a different role for your real agent, pass
--fixed-role "..."to override (available since this release). - The bench startup log now includes
role=...alongside the existingsystem~N charsandtools~N charslines, so any future regression in preset-pinning is visible at first glance.
The fix alone wouldn't have been measurable without the bench infrastructure to actually surface cache reads/writes. Also included:
direct_zenprovider — OpenCode-ZEN gateway (Anthropic-native API format,x-api-keyauth,opencode-cli/0.5.0User-Agent to avoid Cloudflare WAF 1010).direct_anthropicprovider — direct Anthropic API path for users withANTHROPIC_API_KEY.- Cache-control marker wiring in
run_one— sends the stable prefix as a separatesystem[]content block withcache_control: {type: "ephemeral"}when the provider supports it. - Pricing tables with cache_read/cache_write multipliers — 0.10× / 1.25× for Anthropic, 0.10× / 0.28× for Gemini (Gemini's cache write is cheaper than input — opposite of Anthropic).
--preset-agentCLI flag — loads named presets ofsystem + tools + roleso the bench can simulate real production agent workloads.
fc0e9fa—fix(bench): realistic preset pins role so cache key stays constant280b82a—chore(repo): preserve bench diagnostic scripts under scripts/diagnostics/
When you say "this should be cached", check that every sub-token of the cacheable prefix is actually constant across calls. Sub-section randomization is invisible at the prompt level but lethal at the cache-key level.