feat(llm): enable Bedrock/Anthropic prompt caching for Claude models#772
feat(llm): enable Bedrock/Anthropic prompt caching for Claude models#772seanturner83 wants to merge 3 commits into
Conversation
A Strix scan is a long multi-turn agentic loop that re-sends a large, STABLE prefix every turn — the system prompt plus the tool schemas — while only the conversation tail changes. Without a caching breakpoint that whole prefix is re-tokenised and billed at full input rate on every turn; on Bedrock Claude it's the single biggest lever on scan cost. Measured on a real scan: cache-read went 0% -> 57% once these injection points are set (roughly halving input cost, and the ratio climbs on longer scans where the stable prefix dominates more turns). LiteLLM already implements this end to end: when `cache_control_injection_points` is present in the call kwargs its `AnthropicCacheControlHook` fires and emits the provider-appropriate breakpoint (Anthropic `cache_control`; Bedrock Converse `cachePoint`), honouring Anthropic's 4-breakpoint cap. `LitellmModel` forwards `ModelSettings.extra_args` straight into `litellm.acompletion()`, so passing the points there is all that's needed. We mark the two big stable segments (system prompt + tool_config = 2 of 4 breakpoints, headroom left). Deliberately kept at the LiteLLM-config layer rather than a general ModelSettings caching flag — that's the direction the Agents SDK maintainer prescribed when declining a native `cache_system_prompt` field (openai/openai-agents-python#3008 / #3009): caching is a LiteLLM/provider behaviour, and a ModelSettings flag would let strict OpenAI-compatible paths emit non-standard cache_control parts. Gating on Claude keeps it a strict no-op for every other provider (no injection points -> the hook never fires); only Claude-family routes (Anthropic native, Bedrock, Vertex, OpenRouter -> Claude) honour the marker. Tests: parametrised, non-vacuous — Claude routes (bedrock/native/openrouter) get the two injection points; non-Claude (gpt-5/gemini/o3) get extra_args=None.
Greptile SummaryThis PR enables prompt caching for Claude models through LiteLLM. The main changes are:
Confidence Score: 4/5The Claude settings path should preserve existing LiteLLM arguments before merging.
strix/core/inputs.py Important Files Changed
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
strix/core/inputs.py:144-147
**Existing Extra Arguments Are Replaced**
When a Claude caller supplies `ModelSettings.extra_args`, this final `resolve` replaces that dictionary with the caching dictionary. Unrelated LiteLLM options then disappear from the completion request, so the cache key should be merged into the existing arguments.
```suggestion
if _is_claude_model(model_name):
extra_args = {
**(model_settings.extra_args or {}),
**_claude_prompt_cache_extra_args(),
}
model_settings = model_settings.resolve(
ModelSettings(extra_args=extra_args),
)
```
### Issue 2 of 2
strix/core/inputs.py:188
**Tool Cache Point Can Be Ignored**
The locked LiteLLM path may not recognize `tool_config` as an injection-point location. In that runtime, this entry is silently ignored, so tool schemas are re-tokenized on every turn even though the settings test passes; use a location supported by the locked version or update the dependency with integration coverage.
Reviews (1): Last reviewed commit: "feat(llm): enable Bedrock/Anthropic prom..." | Re-trigger Greptile |
Address Greptile feedback on usestrix#772: - Build extra_args as {**existing, **cache} at the call site rather than leaning on ModelSettings.resolve()'s dict-merge — makes preservation of unrelated LiteLLM options obvious to a reader (resolve() does merge, but it's non-obvious). No behaviour change: make_model_settings builds from scratch so the base extra_args is None today. - Document that an unrecognised injection-point location degrades gracefully (not injected, no error) on older LiteLLM pins; tool_config is honoured by the Bedrock Converse transform on versions that support it (litellm 1.90.1 verified).
|
Thanks @greptile-apps — addressed both in af77695, with a note on where each held up vs. not: Issue 1 (extra_args replaced): Issue 2 (tool_config ignored): on current LiteLLM the Bedrock Converse transform does handle Verified live before filing: |
|
Both issues are cleanly addressed. The explicit A few small observations on the final state:
Otherwise this looks solid. The gating, the merge pattern, the docstring, and the parametrised tests are all in good shape. LGTM. |
The cache breakpoints are gated on _is_claude_model (name contains "claude"), but LiteLLM's AnthropicCacheControlHook only *consumes* cache_control_injection_points for models it recognises as cache-capable via its statically bundled model map. On a Bedrock route whose model isn't in that map, the marker passes straight through and Bedrock's Converse API rejects it outright: ValidationException: cache_control_injection_points: Extra inputs are not permitted — which fails the whole scan at the first LLM call. This bites any Bedrock Claude model LiteLLM hasn't mapped yet (a just-released model), and is made worse when LiteLLM can't refresh its remote model map (e.g. behind a TLS-intercepting corporate proxy) and falls back to a stale local copy. Observed live on bedrock/global.anthropic.claude-sonnet-5. Fix: withhold the marker only for a Bedrock route LiteLLM can't confirm supports prompt caching. Scope is deliberately narrow — Anthropic-native, Vertex, and OpenRouter Claude tolerate/ignore the marker (or LiteLLM maps them under keys we don't resolve), so gating those on confirmed support would DISABLE caching for capable models — the opposite of this PR's intent. Only Bedrock hard-rejects, so only Bedrock is guarded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed a follow-up ( Problem: the injection is gated on the model name containing "claude", but LiteLLM's That fails the whole run at the first LLM call. It bites any Bedrock Claude model LiteLLM hasn't mapped yet (a just-released model), and is worse when LiteLLM can't refresh its remote model map — e.g. behind a TLS-intercepting corporate proxy — and falls back to a stale local copy. I hit it on Fix: withhold the marker only for a Bedrock route LiteLLM can't confirm supports prompt caching ( Added two tests covering both directions (unmapped Bedrock → no marker; unmapped non-Bedrock → keeps marker). |
Summary
Enables Anthropic/Bedrock prompt caching for Claude models by passing LiteLLM's
cache_control_injection_pointsthroughModelSettings.extra_argsinmake_model_settings.A Strix scan is a long, multi-turn agentic loop that re-sends a large, stable prefix every turn — the system prompt plus the tool schemas — while only the conversation tail changes. Without a caching breakpoint that whole prefix is re-tokenised and billed at the full input rate on every turn. On Bedrock Claude this is the single biggest lever on scan cost.
Measured on a real scan:
cache-readwent 0% → 57% with these injection points set — roughly halving input-token cost, and the ratio climbs on longer scans where the stable prefix dominates more turns.Why this shape (not a
ModelSettingsflag)This is deliberately kept at the LiteLLM-config layer rather than a general caching flag on
ModelSettings. That's the direction the Agents SDK maintainer explicitly prescribed when declining a nativecache_system_promptfield:The SDK owner's rationale: caching is LiteLLM/provider-specific, and a
ModelSettingsflag would let strict OpenAI-compatible paths emit non-standardcache_controlparts (not a guaranteed no-op). Applying it here, in the Strix SDK-consumer layer, is exactly that prescribed pattern — and it's where the provider is already known.How it works
LiteLLM implements this end to end: when
cache_control_injection_pointsis present in the call kwargs, itsAnthropicCacheControlHookfires and emits the provider-appropriate breakpoint (Anthropiccache_control; Bedrock ConversecachePoint), honouring Anthropic's 4-breakpoint cap.LitellmModelforwardsModelSettings.extra_argsstraight intolitellm.acompletion(), so passing the points there is all that's required.We mark the two big stable segments (2 of the 4 allowed breakpoints, headroom left):
role: system) — the largest repeated spantool_config) — sizeable and identical every turnSafety / scope
_is_claude_model) → a strict no-op for every other provider: no injection points means the hook never fires, so nothing non-standard reaches OpenAI / AnyLLM / non-Claude endpoints.Tests
tests/test_inputs.py— parametrised, non-vacuous:extra_args=None.ruff clean; no new dependencies (LiteLLM support is already present).