nextlevelbuilder / nextlevelbuilder/goclaw
OpenRouter+Anthropic: prompt caching disabled, ~3-4x cost overhead
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.1k
- Avg merge
- 3d 5h
- Merged PRs (30d)
- 24
Description
Summary
OpenAI-compat provider hardcodes CacheControl: false in capabilities. When OpenRouter routes Anthropic Claude models, no cache_control markers are injected, so Anthropic prompt caching never engages. Result: every call resends the full prompt at full input rate.
Evidence
7-day usage from usage_snapshots on a production deployment:
| Model | Input tok | Cache reads | Calls |
|---|---|---|---|
anthropic/claude-sonnet-4.6 (via openrouter) |
46.5M | 0 | 600 |
deepseek/deepseek-v4-flash (via openrouter) |
28M | 22.6M (80%) | 935 |
DeepSeek caches via the OpenRouter provider's own caching mechanism. Sonnet through OpenRouter gets nothing. Spans for model='anthropic/claude-sonnet-4.6' consistently show ~157k input tokens per call with empty metadata (no cache_read_input_tokens/cache_creation_input_tokens).
Cost impact: 46.5M × $3/M ≈ $140/week. With 80% cache hit (matching DeepSeek's ratio): ~$40/week. ~$100/week wasted per tenant. Per-call observed cost ~$0.23 average, ~$0.47 peak.
Root Cause
internal/providers/openai_config.go:99:
func (p *OpenAIProvider) Capabilities() ProviderCapabilities {
return ProviderCapabilities{
...
CacheControl: false, // hard-disabled for all openai-compat providers
...
}
}
internal/providers/middleware_cache.go only injects OpenAI-native prompt_cache_key / prompt_cache_retention and explicitly bails for proxies via isOpenAINativeEndpoint.
internal/providers/anthropic.go:117 correctly sets CacheControl: true and internal/providers/anthropic_request.go injects cache_control: {type: ephemeral} on system block + last tool + recent messages — but only when the agent is bound to a native Anthropic provider, not OpenRouter.
OpenRouter supports the cache_control extension for Claude models in its OpenAI-compat schema (passthrough to Anthropic). The markers just need to be sent.
Suggested Fix
Two paths:
- Conditional capability + injection in openai-compat path. When
providerType == "openrouter"(or detected Anthropic upstream) AND model matchesanthropic/*orclaude*, setCacheControl: trueand apply the same block-level cache markers used inanthropic_request.go(system text block + last tool + last 1–2 user messages). - Generic OpenAI-compat cache extension. Some OpenAI-compat backends (LiteLLM, OpenRouter) accept
cache_controlon message content blocks. Gate behind a per-provider setting (settings.cache_control_passthrough) so users can opt in for backends that support it.
Option 1 is targeted and matches what anthropic.go already does — least churn, highest impact.
Workaround
Configure a native Anthropic provider in the dashboard and re-point Claude-using agents to it. Native adapter handles caching automatically. Loses OpenRouter's load-balancing/failover, but cuts cost ~70%.
Reproduce
- Configure OpenRouter as the only LLM provider.
- Create an agent with
provider=openrouter,model=anthropic/claude-sonnet-4.6. - Run any multi-turn workflow with stable system prompt + tools.
- Check
usage_snapshots—cache_read_tokensandcache_create_tokenswill be 0 across all calls.
Affected Code
internal/providers/openai_config.go:99— capability flaginternal/providers/openai_request.go— request body builder, no Anthropic-aware branchinternal/providers/middleware_cache.go— bails on non-OpenAI endpoints
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with internal/providers/openai_config.go, openai_request.go, and middleware_cache.go, then compare their behavior with internal/providers/anthropic.go and anthropic_request.go. Reproduce the OpenRouter Claude workflow and inspect usage_snapshots for cache reads and writes. Done means the selected supported path sends the expected cache markers and usage reflects caching without breaking other OpenAI-compatible providers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100