feat(anthropic): add enable_anthropic_prompt_caching for automatic cache_control injection - #33573
Conversation
…che_control injection Anthropic only caches a prompt when the request carries explicit cache_control breakpoints, unlike OpenAI where prompt caching is automatic and needs no configuration. Today litellm can inject those breakpoints server-side, but only when an admin hand-writes cache_control_injection_points into a model's litellm_params (or router_settings.default_litellm_params). Clients such as Claude Code and Claude Desktop never set cache_control themselves, and the admin recipe is easy to miss, so Anthropic traffic through the proxy silently pays full price on every repeated prefix. This adds an opt-in litellm_settings flag, enable_anthropic_prompt_caching. When it is on and the request has no injection points configured and no client-supplied cache_control, litellm synthesizes a default pair of breakpoints (the system prompt and the trailing turn) so the stable prefix is cached while the breakpoint advances with the conversation. It is wired into both surfaces: /chat/completions seeds the points before the existing prompt-management gate, and /v1/messages resolves them in maybe_inject_cache_control, so the existing AnthropicCacheControlHook applies them unchanged and keeps its four-block cap and its refusal to overwrite client breakpoints. The default is off, so no existing deployment changes behavior. Injection is gated to providers that actually consume cache_control markers (anthropic and bedrock) and to models the cost map flags as supporting prompt caching; note that supports_prompt_caching alone is not a sufficient gate, since OpenAI, Azure and Gemini models report it as well but never take cache_control markers. The default ttl is Anthropic's 5 minute ephemeral cache, with an optional anthropic_prompt_caching_ttl of "5m" or "1h"; ttl is also added to ChatCompletionCachedContent, which the bedrock and anthropic transforms already read at runtime but the type never declared Resolves LIT-4478
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds
Confidence Score: 5/5Safe to merge. The flag is off by default, so no existing deployment changes behavior. All injection logic is gated behind multiple explicit conditions and the existing four-block cap remains enforced by the upstream hook. The implementation is narrow and well-scoped: provider and model-capability checks prevent injection outside the intended target, the stand-down path when client-supplied cache_control is present is exercised by tests, and the double-seeding path through acompletion → completion is a no-op due to the guard. No pre-existing tests were modified, and all new tests are pure-mock or subprocess-only with no network calls. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/init.py | Adds two new module-level attributes: enable_anthropic_prompt_caching (bool, read from env) and anthropic_prompt_caching_ttl (Literal["5m","1h"] |
| litellm/integrations/anthropic_cache_control_hook.py | Adds four new static methods for default injection point management. Logic is sound: provider gating, supports_prompt_caching check, client cache_control detection (messages + system list + tools), and a no-op guard when explicit points are already configured all work correctly. |
| litellm/main.py | Seeds cache_control_injection_points early in both acompletion and completion before the prompt-management gate. The seeding in acompletion flows through **kwargs into completion; the second seeding call in completion is a no-op because the guard checks non_default_params.get("cache_control_injection_points"). |
| litellm/llms/anthropic/experimental_pass_through/messages/handler.py | Passes model, custom_llm_provider, and tools to the updated maybe_inject_cache_control signature — minimal, targeted change that enables auto-injection on the /v1/messages path. |
| litellm/types/llms/openai.py | Adds ttl: NotRequired[Literal["5m", "1h"]] to ChatCompletionCachedContent. The existing Bedrock transforms already read this field at runtime; this PR just adds the missing type annotation. |
| tests/test_litellm/integrations/test_anthropic_cache_control_hook.py | Adds comprehensive mock-only tests. TestAnthropicPromptCachingEnvVars uses subprocess to re-import litellm with different env vars — no real network calls made. Coverage is good: default-off, bedrock, non-Anthropic gate, tool-caching stand-down, TTL override, and both API paths. |
| ui/litellm-dashboard/src/lib/http/schema.d.ts | Auto-generated OpenAPI schema file updated to reflect the new ttl field on ChatCompletionCachedContent. |
Reviews (3): Last reviewed commit: "fix(anthropic): stand down when the clie..." | Re-trigger Greptile
| control = AnthropicCacheControlHook._default_control() | ||
| points: list[CacheControlInjectionPoint] = [ | ||
| CacheControlMessageInjectionPoint(location="message", role="system", index=None, control=control), | ||
| CacheControlMessageInjectionPoint(location="message", role=None, index=-1, control=control), |
There was a problem hiding this comment.
Medium: Cross-tenant prompt membership oracle
This breakpoint caches the complete request-controlled conversation in the upstream workspace/account cache. Because LiteLLM returns cache_read_input_tokens and cache_creation_input_tokens, an authenticated user sharing those provider credentials can submit an exact candidate prefix and determine whether another tenant recently submitted it. Limit automatic caching to operator-controlled static prefixes, or only enable the trailing-turn breakpoint when upstream credentials and caches are tenant-scoped.
There was a problem hiding this comment.
Good catch, and the exposure change is real, so I have documented it at the point of decision. One correction on the suggested mitigation though
Confirmed the flag broadens exposure. With it off, a client that never sends cache_control has nothing cached, so there is nothing to probe. With it on, the same request caches, which is what makes the prefix probeable:
flag off -> cache_creation_input_tokens 0
flag on -> cache_creation_input_tokens 13667
The suggested mitigation does not close it here. Limiting injection to operator-controlled static prefixes assumes the system prompt is operator-controlled, but system is a client-supplied request parameter on /v1/messages (handler.py:204), and Claude Code supplies its own. So a system-only breakpoint would still cache client-controlled content on the shared account while removing most of the feature value
The underlying property is Anthropic prompt caching itself: a prefix is cached against the upstream credentials that sent it, not per end user. Any caller can already self-serve a cache_control breakpoint in their own request body, so the gateway cannot make the cache tenant-scoped; only issuing per-tenant upstream credentials does that
Given that, this stays an operator decision, which is why the flag is opt-in and off by default. What was missing was surfacing the tradeoff where the decision is made, so the setting description and the env var docs now state that the provider caches against the upstream credentials rather than per end user, and to leave it off if callers sharing credentials must not learn whether another caller recently sent a given prompt
PR overviewThis PR adds an Anthropic prompt-caching integration that can automatically inject There is one open security concern around automatic caching of request-controlled conversation content when multiple tenants share the same upstream Anthropic credentials or cache namespace. In that configuration, an authenticated user could use returned cache token counters to test whether another tenant recently submitted an exact prompt prefix. No issues have been addressed yet, so the PR still needs a scoping or restriction change before this behavior is safe for shared-provider deployments. Open issues (1)
Fixed/addressed: 0 · PR risk: 6/10 |
Both enable_anthropic_prompt_caching and anthropic_prompt_caching_ttl are now read from LITELLM_ENABLE_ANTHROPIC_PROMPT_CACHING and LITELLM_ANTHROPIC_PROMPT_CACHING_TTL at import, so the flag can be turned on without a config file. An unsupported ttl falls back to the provider default rather than reaching the provider verbatim
|
@greptileai rereview |
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
bugbot run |
_request_has_cache_control only looked at messages and system, so a client that marks cache_control on tools alone did not suppress auto-injection. Tool breakpoints count toward the provider's four-block limit, so three of them plus the two injected here is five, which Anthropic rejects. Thread tools through both entry points and treat a client-marked tool as the stand-down signal it already is for messages and system.
|
bugbot run |
|
@greptileai rereview |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 53c285a. Configure here.
Relevant issues
cache_controlbreakpoints, unlike OpenAI where prompt caching is automatic. Clients such as Claude Code and Claude Desktop never set them, so Anthropic traffic through the proxy silently pays full price on every repeated prefix. The only way to get caching was for an admin to hand-writecache_control_injection_pointsinto each model'slitellm_params, a recipe that is easy to miss and that end users cannot apply themselvesenable_anthropic_prompt_cachingflag, settable from a config file or an environment variable (and from the Admin UI in feat(ui): configure Anthropic automatic prompt caching from the Admin UI #33581). When it is on, litellm injects a default pair of breakpoints covering the system prompt and the trailing turn for Anthropic and Bedrock Claude models, and stands down entirely when the request already carries its owncache_control. Default is off, so no existing deployment changes behaviorLinear ticket
Resolves LIT-4478
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy on localhost:4000 against the real Anthropic API, one Anthropic model and a system prompt of roughly 7.9k tokens, sent twice so the second call can hit the cache
Before (
enable_anthropic_prompt_caching: false, today's default behavior)Every call re-reads the same 7940 token prefix at full price and nothing is ever cached, on either surface
After (
enable_anthropic_prompt_caching: true)/v1/chat/completionswrites the cache on the first call and reads it back on the second/v1/messages(the surface Claude Code and Claude Desktop use) against a cold prefix, same resultWhen the client sends its own
cache_control, the flag stands down and the client's breakpoint drives the cache, so nothing is double-injectedSpend, straight from the spend logs
The same rows show why this matters, and confirm the cache tokens reach the spend log for both surfaces, which is what the Logs UI request drawer already renders
An uncached call costs 0.015920, a cache write costs 0.019889 (the documented 1.25x premium for the 5 minute cache) and every subsequent cache read costs 0.001632, roughly 90% less. The flag pays for itself after a single repeat
Environment variables
The flag can also be turned on without a config file, for deployments that template the environment instead. Same config as above with
litellm_settingsomitted entirely, so the environment is the only thing that can enable cachingAnthropic was intermittently returning
overloaded_errorwhile this was captured and was not serving cache reads for any request, including ones sent straight toapi.anthropic.comwith client-suppliedcache_controland litellm entirely out of the loop, so these runs show the write side only. The read side is covered by the runs abovelitellm_settingsstill wins when both are set, since the config is applied after importType
🆕 New Feature
Changes
Anthropic only caches a prompt when the request carries explicit
cache_controlbreakpoints, unlike OpenAI where prompt caching is automatic and needs no configuration. litellm can already inject those breakpoints server-side, but only when an admin hand-writescache_control_injection_pointsinto a model'slitellm_paramsor intorouter_settings.default_litellm_params. Clients such as Claude Code and Claude Desktop never setcache_controlthemselves and the admin recipe is easy to miss, so Anthropic traffic through the proxy silently pays full price on every repeated prefixThis adds an opt-in
litellm_settingsflag,enable_anthropic_prompt_caching. When it is on, and the request has no injection points configured and no client-suppliedcache_control, litellm synthesizes a default pair of breakpoints covering the system prompt and the trailing turn, so the stable prefix stays cached while the breakpoint advances with the conversation. Both surfaces are wired to one helper:/chat/completionsseeds the points just before the existing prompt-management gate, and/v1/messagesresolves them insidemaybe_inject_cache_control. The existingAnthropicCacheControlHookthen applies them unchanged, so the four-block cap and its refusal to overwrite client breakpoints both still holdThe default is off, so no existing deployment changes behavior. Injection is gated to the providers that actually consume
cache_controlmarkers, meaninganthropicandbedrock, and to models the cost map flags as supporting prompt caching.supports_prompt_cachingon its own is not a sufficient gate: OpenAI, Azure and Gemini report it too, but they do not treatcache_controlthe way Anthropic does. A real openai.com host strips the marker, so injecting there is a harmless no-op, while Vertex and Google AI Studio Gemini consume it and convert it into Gemini context caching, which is a different mechanism with its own semantics and cost profile. Gating on that field alone would therefore silently switch on another provider's caching behind a flag named for Anthropic, which is why the provider check sits alongside it. Claude onvertex_aiis deliberately left out for now, because Gemini on Vertex reports the same capability while using a different caching mechanism, and separating them needs a model-level check worth doing on its ownThe default ttl is Anthropic's 5 minute ephemeral cache, which is both their API default and what Claude Code uses, with an optional
anthropic_prompt_caching_ttlof"5m"or"1h"for long agentic sessions. 1h doubles the write premium instead of the 1.25x above, so it is opt-in rather than the default.ttlis also added toChatCompletionCachedContent, a field the bedrock and anthropic transforms already read at runtime but the type never declaredBoth settings can also be set from the environment, with
LITELLM_ENABLE_ANTHROPIC_PROMPT_CACHINGandLITELLM_ANTHROPIC_PROMPT_CACHING_TTL, so a deployment that templates env vars rather than a config file can turn this on without one.litellm_settingswins over the environment when both are present, because the config is applied after import. An unsupported ttl value falls back to the provider default instead of reaching the provider verbatimA note on cache sharing
Worth stating explicitly, since this flag makes it apply to everyone rather than only to callers who opted in themselves.
Provider prompt caching is not per end user. The provider caches a prefix against the upstream credentials that sent it, and any request repeating that prefix exactly reuses it. That sharing is the point, and it is what makes this worth turning on: a long system prompt cached by one user is reused by everyone else on the same credentials. The consequence is that a caller who reproduces a prefix exactly can tell that someone else on those credentials sent it recently, because the response reports
cache_read_input_tokensand a cache hit is also faster.None of that is new behavior. It is how Anthropic prompt caching works, it applies today to anyone who sets
cache_controlthemselves or configurescache_control_injection_points, and hiding the usage fields would not change it because the latency difference carries the same signal. What this flag changes is the population: with it off, a client that never sendscache_controlhas nothing cached, so nothing about it is observable; with it on, every request through the gateway is cached and therefore observable. The exposure is bounded by the fact that a prefix must be reproduced exactly and providers will not cache a prefix below a minimum size (model-dependent for Anthropic, currently 1k to 4k tokens), so it reveals whether a prompt the caller already holds was recently sent, rather than revealing its contents.The isolation boundary is the provider account, which is not something the gateway can synthesize; separate credentials per tenant is the only real answer. So this stays an operator decision, and the flag is off by default. The setting description and the env var docs both state it, and the auto-inject checkpoints doc now carries a section on it, since the behavior predates this PR and was not documented anywhere.
QA runbook
enable_anthropic_prompt_cachingunset. Send the same large prompt (it must clear the provider's minimum cacheable prefix, up to 4k tokens on current Anthropic models, or nothing will cache) twice to/v1/chat/completionsand to/v1/messages, and confirmcache_creation_input_tokensandcache_read_input_tokensstay at 0enable_anthropic_prompt_caching: trueunderlitellm_settings, restart, and repeat. The first call should reportcache_creation_input_tokensgreater than 0 and the second should reportcache_read_input_tokensgreater than 0cache_controland confirm the breakpoint count does not grow and the client's own TTL is preservedcache_controlappears in the outgoing requestanthropic_prompt_caching_ttl: "1h"and confirmcache_creation.ephemeral_1h_input_tokensis what movesLITELLM_ENABLE_ANTHROPIC_PROMPT_CACHING=truein the environment, and confirm caching engages exactly as in step 2Final Attestation
Note
Medium Risk
Changes outbound request shaping and billing-related caching behavior for Anthropic/Bedrock when enabled; mitigated by default-off, provider/model gating, and stand-down when clients supply cache_control.
Overview
Adds an opt-in
enable_anthropic_prompt_cachingsetting (config, envLITELLM_ENABLE_ANTHROPIC_PROMPT_CACHING, default off) plus optionalanthropic_prompt_caching_ttl(5m/1h) so LiteLLM can inject Anthropiccache_controlbreakpoints without per-modelcache_control_injection_points.When enabled,
AnthropicCacheControlHooksynthesizes two default breakpoints (system + last message) for anthropic and bedrock models that support prompt caching, and stands down if the client already setcache_controlon messages, system, or tools./chat/completionsseedscache_control_injection_pointsbefore prompt-management hooks;/v1/messagesresolves defaults insidemaybe_inject_cache_control.ChatCompletionCachedContentgains an optionalttlfield; tests cover gating, tool stand-down, and env parsing.Reviewed by Cursor Bugbot for commit 53c285a. Bugbot is set up for automated code reviews on this repo. Configure here.