feat(agent): request and surface Anthropic prompt caching (+ MCP proxy/TLS passthrough) - #3463
Open
atishpatel wants to merge 2 commits into
Open
feat(agent): request and surface Anthropic prompt caching (+ MCP proxy/TLS passthrough)#3463atishpatel wants to merge 2 commits into
atishpatel wants to merge 2 commits into
Conversation
buzz talks to the Databricks Anthropic gateway with the correct endpoint and wire format, but never asked for caching. The Anthropic Messages API does not cache unless a request carries a cache_control breakpoint, and the Databricks gateway (a third-party surface, like Bedrock/Vertex) does not auto-cache. So cache_read_input_tokens came back 0 on every call and the ~10x cache-read discount was never claimed. Verified live 2026-07-28: two byte-identical calls without cache_control both read 0; adding one marker moved 121,625 tokens to a 0.1x cache read and halved latency. anthropic_body() now emits ephemeral cache_control breakpoints, gated by BUZZ_AGENT_PROMPT_CACHING (default on): - static prefix: cache_control on the system block, which (prefix order being tools -> system -> messages) caches tools + system together - rolling tail + leapfrog: cache_control on the last block of the last TWO messages, so the append-only history re-reads the prior turn's prefix from cache while keeping consecutive breakpoints inside Anthropic's 20-block lookback window even as tool parallelism rises An empty system prompt stays a bare string (Anthropic rejects empty text blocks). Below-threshold prefixes are silently not cached, so the flag is safe on by default; BUZZ_AGENT_PROMPT_CACHING=0 restores the previous behaviour. Also plumbs the cache split end-to-end so accounting can price it: - LlmResponse gains cached_input_tokens (a subset of input_tokens, never an addition); parse_anthropic/parse_openai/parse_responses each populate it - usage_first() reads the cache count wherever a provider puts it: flat cache_read_input_tokens (Anthropic), prompt_tokens_details.cached_tokens (OpenAI chat), input_tokens_details.cached_tokens (Responses) — taking the first present value, never a sum, since these are alternate spellings of one number and Databricks reports several at once - fixes a Databricks MLflow double-count: prompt_tokens is already inclusive of the flat cache_read_input_tokens it also reports, so openai_chat_input_tokens now reads prompt_tokens alone instead of summing (verified on databricks-glm-5-2, where prompt_tokens + completion == total) - the per-turn/per-session accumulators and the goose usage_update payload carry accumulatedCachedInputTokens; buzz-acp deserializes it (serde default 0 for goose, which does not send it) and logs cached=<n> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Atish Patel <atish@squareup.com>
buzz-agent env_clear()s the environment of each MCP child, and the passthrough allowlist carried no proxy or TLS-trust variables. On a host whose only route out is a CONNECT proxy this does not degrade the tools, it blinds them: apt, curl, pip and git connect directly instead, the egress firewall resets the socket, and the agent reports "Connection reset by peer" and concludes the environment has no network — indistinguishable in the transcript from a task that is genuinely offline. Add both spellings of the proxy vars (HTTP_PROXY/HTTPS_PROXY/NO_PROXY/ALL_PROXY and lowercase) to PASSTHROUGH_ENV: curl and git read lowercase, most Go/Python tooling reads uppercase, and libcurl deliberately ignores uppercase HTTP_PROXY, so keeping only one form silently breaks half the toolchain. Also pass SSL_CERT_FILE/SSL_CERT_DIR: a TLS-terminating proxy presents its own CA, and an image whose trust store lacks it fails every https fetch with a verification error — the same class of failure, the parent configured correctly and the child unable to see it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Atish Patel <atish@squareup.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent, self-contained fixes to
buzz-agent/buzz-acp, split out of the benchmark branch so they can land while the harness work continues:cache_controlbreakpoint, so on the Databricks Anthropic routecache_read_input_tokenswas structurally always 0 and the ~10× cache-read discount was never claimed. This teachesanthropic_body()to mark the cacheable prefix, and plumbs the cache split end-to-end so accounting can price it.Why the caching gap matters
The Anthropic Messages API does not cache unless the request carries a
cache_controlbreakpoint, and the Databricks AI Gateway — a third-party proxy in front of the model, in the same category as Bedrock/Vertex — does not auto-cache (only the first-party Anthropic API and Claude-on-AWS do zero-config caching). So every request was billed cold.Measured live against the Databricks gateway (
databricks-claude-opus-5, 2026-07-28), the same call with and without a singlecache_controlmarker:input_tokenscache_creationcache_readcache_control, two byte-identical callsOne marker moved 121,625 tokens from full-price input to a 0.1× cache read and roughly halved latency (a clean, isolated ~2.07× prefill speedup on this single-threaded microbenchmark). The gateway honours
cache_control; buzz simply never sent it.At fleet scale this was a real budget item. Across matched Terminal-Bench solo sweeps (89 tasks,
-n 20, before the fix), the two OpenAI-route models independently landed at ~86–87% cache reads — the expected shape for an agentic loop, where system + tools + append-only history repeat every turn — while the Anthropic route returned a hard 0% on every receipt:gpt-5-6)gpt-5-6)claude-opus-5)Applying luna's measured 87% read rate to the opus token counts at list prices (
input $5/M,cached_input $0.5/M,output $25/M) puts the opus run at ~$32.53 vs the $81.31 actually paid — a60% overspend on those 49 trials ($89 on a full sweep). That is an upper bound (it prices every cached token at the 0.1× read rate and ignores the 1.25× write premium), and the opus discount is structurally smaller than luna/sol's because opus emits ~3.5× more uncacheable output per trial, which sets a floor on what caching can recover.There is also a plausible second-order effect: Databricks appears to meter its per-minute rate limit on uncached input tokens, so the missing cache also cost rate-limit headroom — the opus endpoint lost 63% of its trials to fatal 429s while running alone at one-third of a GPT endpoint's raw throughput. This is a hypothesis, not a proven mechanism (the only zero-cache condition is also the only Anthropic endpoint), but it is the reading that explains the throttling with one rule instead of two.
Post-fix results (provisional — first trials of an in-flight re-run)
Re-running the opus condition on a build with the fix, at unchanged
-n 20, all three predicted effects show up:4a955a858)3bef1f6a)cost_usd_no_cache_discount / cost_usd-n 20)On 8 tasks matched by name across the two runs, cost fell $8.36 → $1.77 (4.71×) and wall-clock 12,423 s → 1,085 s (11.45×). To be clear about attribution: ~2× of that is the clean prefill saving from caching itself; the rest is second-order — cached requests burn far less rate-limit budget, so they stall less and redo less destroyed work. The 11.45× is a system-level result specific to this throttled workspace, not a caching benchmark. Quality held (7/8 solved in each run). A controlled low-
-nA/B (neither arm hitting a 429), which theBUZZ_AGENT_PROMPT_CACHINGopt-out exists to enable, is still owed before this becomes a published claim.What changed
1. Request caching (
llm.rs,config.rs)anthropic_body()emits ephemeralcache_controlbreakpoints, gated byBUZZ_AGENT_PROMPT_CACHING(default on,=0to opt out):systemblock. Prefix order istools → system → messages, so this single marker caches tools + system together. Byte-identical on every turn of a run, and survives a context handoff (system/tools come from cfg/mcp, notself.history).An empty system prompt stays a bare string (Anthropic rejects empty text blocks), and below-threshold prefixes are silently not cached, so the flag is safe on by default.
2. Surface the cache split end-to-end — the plumbing (
types.rs,llm.rs,agent.rs,lib.rs,usage.rs,acp.rs)This is the part that makes gaps like the one above visible instead of silent. A consumer that prices all of
input_tokensat the full rate can't tell a route that's caching from one that isn't — the total looks right either way. So:LlmResponsegainscached_input_tokens(a subset ofinput_tokens, never an addition);parse_anthropic/parse_openai/parse_responseseach populate it.usage_first()helper reads the cache count wherever a provider hides it — flatcache_read_input_tokens(Anthropic),prompt_tokens_details.cached_tokens(OpenAI chat),input_tokens_details.cached_tokens(Responses) — taking the first present value, never a sum. Reading only flat keys is exactly why the OpenAI route's nestedcached_tokenshad also been going unclaimed:prompt_tokensis already inclusive, so the total looked correct while the discount silently went unreported.usage_updatepayload now carryaccumulatedCachedInputTokens;buzz-acpdeserializes it (serdedefault0for goose, which doesn't send it) and logscached=<n>.3. Fix a Databricks MLflow-route double-count (
llm.rs)The Databricks MLflow route reports the flat Anthropic-spelled
cache_read_input_tokensalongside an already-inclusiveprompt_tokens, so the old code summed them and nearly doubled the count — inflating both the context-budget gate and cost.openai_chat_input_tokens()now readsprompt_tokensalone. Verified on a livedatabricks-glm-5-2response whereprompt_tokens + completion == totalproves inclusivity. (Anthropic's native route genuinely excludes the cache fields and is still summed — the two never collide, becauseclaude*models route to the Anthropic path.)4. Proxy + TLS-trust passthrough into MCP tools (
mcp.rs) — independent fixbuzz-agentenv_clear()s each MCP child, and the allowlist carried no proxy/TLS vars. On a proxy-only host that doesn't degrade the tools, it blinds them: apt, curl, pip, git connect directly, the egress firewall resets the socket, and the agent reports "Connection reset by peer" — indistinguishable from a genuinely offline task. Adds both spellings ofHTTP(S)_PROXY/NO_PROXY/ALL_PROXY(curl/git read lowercase; Go/Python read uppercase; libcurl ignores uppercaseHTTP_PROXY) plusSSL_CERT_FILE/SSL_CERT_DIRfor TLS-terminating proxies that present their own CA.Testing
cargo fmt --all -- --check,cargo clippy -p buzz-agent -p buzz-acp --all-targets -- -D warnings— clean.cargo test -p buzz-agent -p buzz-acp— all green (632 + 299 lib tests plus integration suites, 0 failures). New tests cover: the three breakpoints and the disabled/empty-system/single-message edge cases; nested-vs-flat cache parsing for all three routes; the Databricks inclusive-prompt_tokensfix; wire deserialization ofaccumulatedCachedInputTokens; and the proxy/TLS passthrough allowlist.Relationship to the benchmark branch
These are the non-
benchmarks/changes frombenchmark/harness-accounting-and-solo, lifted onto a clean base offmainso they can merge independently. The full design write-up (platform matrix, cost model, fleet measurement, post-fix results, and the rate-limit hypothesis) lives in that branch'sdocs/07-anthropic-prompt-caching.md.🤖 Generated with Claude Code