Repository navigation
fix(anthropic): keep cache_control for Gemini targets on /v1/messages and normalize Anthropic ttl units - #41938
Conversation
…ken requirements to prevent 400s
… and normalize Anthropic ttl units
…Google's live cachedContents API
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
| def target_consumes_cache_control(cls, model: str) -> bool: | ||
| return cls.is_anthropic_claude_model(model) or cls.is_bedrock_arn_model(model) or "gemini" in model.lower() |
There was a problem hiding this comment.
Gemini support is inferred from the model name, violating the requirement to store model-specific flags in metadata before merging.
Rule Used: What: Do not hardcode model-specific flags in the codebase. Instead, put them in model_prices_and_context_window.json and then read them in via get_model_info Why: Prevents need for users to upgrade litellm each time a new model supports this featu... (source)
There was a problem hiding this comment.
Provider capability, not a model flag: the Gemini transformation consumes cache_control, the cost map has no such field, and the claude check matches by name
There was a problem hiding this comment.
You're right — my earlier finding does not apply here. target_consumes_cache_control() determines whether the Anthropic bridge should preserve a target-specific request field; it is not a model capability flag used for pricing, limits, or cost-map behavior. The existing Claude and Bedrock checks already use model identity for this routing decision, so adding Gemini consistently extends that target detection. I’m withdrawing this finding.
…the explanatory docstrings
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
bugbot run |
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 5ad1847. Configure here.
Supersedes #32366 at its head 2303379, moved from the fork branch to an internal
litellm_branch so CircleCI runs thellm_translationandlocal_testingsuites; the contributor's commits and authorship are carried over unchanged.TLDR
Problem this solves:
/v1/messagesnever gets prompt caching/v1/messagesbridge droppedcache_controlfor every non-Claude target1hor5m; Gemini only accepts3600sHow it solves it:
/v1/messageskeepscache_controlwhen the target model is a Gemini model<seconds>sformUser Flow
Before: a developer running Claude Code against a Gemini model through the gateway pays for the whole system prompt on every turn, and a short cached block on Gemini 2.5 Pro fails outright
ANTHROPIC_BASE_URL=https://litellm-domainandANTHROPIC_MODEL=gemini-25-flash, then ask a question"cache_control": {"type": "ephemeral"}usagecounts every prompt token asinput_tokensand carries nocache_read_input_tokens, and the next turn re-sends the whole system prompt at full price/costand see0 cache read, 0 cache writeand "no prompt caching reported by the API""ttl": "5m"to POST https://litellm-domain/v1/messages and gets the same uncached usage back; GET https://generativelanguage.googleapis.com/v1beta/cachedContents on their Gemini key lists no cache for itcache_controlto Gemini 2.5 Pro through POST https://litellm-domain/v1/chat/completions or POST https://litellm-domain/v1/responsesCached content is too small. total_token_count=1507, min_total_token_count=2048After: the same turns read the system prompt from Gemini's cache, the ttl lands as sent, and the short block on 2.5 Pro goes through as a plain request
ANTHROPIC_BASE_URL=https://litellm-domainandANTHROPIC_MODEL=gemini-25-flash, then ask a question"cache_control": {"type": "ephemeral"}usagenow carries the system prompt ascache_read_input_tokensand only the new text asinput_tokens/costand see28.4k cache readand "46% of input tokens from cache", at a lower dollar figure than before"ttl": "5m"request comes back withcache_read_input_tokens, and GET https://generativelanguage.googleapis.com/v1beta/cachedContents lists a cache whoseexpireTimeis five minutes after itscreateTimeusageRelevant issues
Ref #29810, Ref #29166
Affected release
Linear ticket
Resolves LIT-5373
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@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
Both legs ran the same way and differ only in the commit the proxy was booted from. Each proxy ran
python litellm/proxy/proxy_cli.py --config config.yaml --num_workers 2 --detailed_debug(2 uvicorn workers, no Redis, no database) withLITELLM_LOCAL_MODEL_COST_MAP=Trueso each side reads the cost map at its own commit. Before is the merge base 5611607 on port 44911, After is the tip 5ad1847 on port 43175. Every request went to Google's real Gemini API through thegemini/provider and cost real moneyPayloads. The
/v1/messagesones carry a 27,823-character generated handbook as the system block (about 5.2k tokens, above every Gemini minimum);messages-ttl-5m.jsonis the same file with"ttl": "5m". The 2.5 Pro ones carry an 8k-character slice of it (1507 tokens, under 2.5 Pro's 2048 minimum)Claude Code v2.1.278 was launched interactively in tmux against each proxy with
env -u ANTHROPIC_API_KEY ANTHROPIC_BASE_URL=http://localhost:<port> ANTHROPIC_AUTH_TOKEN=sk-lit5373-qa ANTHROPIC_MODEL=gemini-25-flash ANTHROPIC_SMALL_FAST_MODEL=gemini-25-flash claude, given the same two prompts (Reply with one word: hello, thenworld), then/costBefore (5611607)
/v1/messages
for i in 1 2; do curl -s http://localhost:44911/v1/messages -H "Authorization: Bearer sk-lit5373-qa" -H "Content-Type: application/json" -d @messages-ttl-1h.json | jq -c .usage; done{"input_tokens":5213,"output_tokens":167}then{"input_tokens":5213,"output_tokens":130}: the whole system block is billed as input both times, no cache fields@messages-ttl-5m.json{"input_tokens":5213,"output_tokens":252}twice, no cache fields/v1/chat/completions
curl -s -w "\n%{http_code}\n" http://localhost:44911/v1/chat/completions -H "Authorization: Bearer sk-lit5373-qa" -H "Content-Type: application/json" -d @chat-25pro.json{"error": {"message": "litellm.BadRequestError: GeminiException BadRequestError - {\n \"error\": {\n \"code\": 400,\n \"message\": \"Cached content is too small. total_token_count=1507, min_total_token_count=2048\",\n \"status\": \"INVALID_ARGUMENT\"\n }\n}\n. Received Model Group=gemini-2.5-pro\nAvailable Model Group Fallbacks=None", "type": "invalid_request_error", "param": null, "code": "400"}}/v1/responses
curl -s -w "\n%{http_code}\n" http://localhost:44911/v1/responses -H "Authorization: Bearer sk-lit5373-qa" -H "Content-Type: application/json" -d @responses-25pro.json"Cached content is too small. total_token_count=1507, min_total_token_count=2048"Claude Code
Reply with one word: hello, thenworld/costreadsgemini-25-flash: 67.2k input, 257 output, 0 cache read, 0 cache write ($0.3426)and0% of input tokens from cache · no prompt caching reported by the APIAfter (5ad1847)
/v1/messages
for i in 1 2; do curl -s http://localhost:43175/v1/messages -H "Authorization: Bearer sk-lit5373-qa" -H "Content-Type: application/json" -d @messages-ttl-1h.json | jq -c .usage; done{"input_tokens":11,"output_tokens":252,"cache_read_input_tokens":5204}twice: the system block is read from Gemini's cache, only the question is billed as input@messages-ttl-5m.json{"input_tokens":11,"output_tokens":252,"cache_read_input_tokens":5204}then{"input_tokens":11,"output_tokens":227,"cache_read_input_tokens":5204}curl -s https://generativelanguage.googleapis.com/v1beta/cachedContents -H "x-goog-api-key: $GEMINI_API_KEY" | jq -c '.cachedContents[] | {name, model, createTime, expireTime, tokens: .usageMetadata.totalTokenCount}'1hrequest's cache expires an hour after creation and the5mone five minutes after, so the Anthropic units reached Google as3600sand300s:{"name":"cachedContents/f0rkj0ax8ud3x83y1ze6pbi2gyuuygtgspdmah81","model":"models/gemini-3.8-flash","createTime":"2026-09-19T08:01:58.179908Z","expireTime":"2026-09-19T09:01:57.954397289Z","tokens":5204}{"name":"cachedContents/l0w797dy9t2zwojatn3a42bwp4bfreek2q22hymh","model":"models/gemini-3.8-flash","createTime":"2026-09-19T08:02:04.463005Z","expireTime":"2026-09-19T08:07:04.174079086Z","tokens":5204}{"name":"cachedContents/11gmbsj5b0oqdiohb9dmlxwus1zcw7ikm4bqnrhd","model":"models/gemini-2.5-flash","createTime":"2026-09-19T08:01:53.398979Z","expireTime":"2026-09-19T09:01:52.997352359Z","tokens":14216}(Claude Code's own cache from the case below; it sends no ttl, so Google's default hour applies)/v1/messagesrequests carried a ttl the proxy cannot forward (5,["1h"],"", 5,000xcharacters,0h,-5m,0.0000000001s, a 400-digit hour count,87660001h,315576000000s,251700000000s) or one at the edge (1.5h,999999h,61320000h): every one answered HTTP 200 withcache_read_input_tokensand the unforwardable ones got Google's default hour, while the edge ones landed as sent (5400s, a cache expiring in 2140, a cache expiring in 9022; the long-lived ones were deleted afterwards)/v1/chat/completions
curl -s -w "\n%{http_code}\n" http://localhost:43175/v1/chat/completions -H "Authorization: Bearer sk-lit5373-qa" -H "Content-Type: application/json" -d @chat-25pro.json"finish_reason": "stop", contentThis operations handbook explains how to configure various gateway features like billing, routing, and caching, detailing how each one evaluates requests, handles failures, and reports metrics.usage:{"completion_tokens": 799, "prompt_tokens": 1516, "total_tokens": 2315, "completion_tokens_details": {"reasoning_tokens": 767, "text_tokens": 32}, "prompt_tokens_details": {"text_tokens": 1516}}, no cache fields because the block is under 2.5 Pro's minimum and was sent uncached/v1/responses
curl -s -w "\n%{http_code}\n" http://localhost:43175/v1/responses -H "Authorization: Bearer sk-lit5373-qa" -H "Content-Type: application/json" -d @responses-25pro.json"status": "completed", output textThis operations handbook explains how various gateway features like billing, routing, and caching are configured, how they process requests, and how they report errors and metrics.usage:{"input_tokens": 1516, "input_tokens_details": {"cached_tokens": 0, "text_tokens": 1516}, "output_tokens": 1318, "output_tokens_details": {"reasoning_tokens": 1287, "text_tokens": 31}, "total_tokens": 2834}Claude Code
Reply with one word: hello, thenworld/costreadsgemini-25-flash: 34.5k input, 223 output, 28.4k cache read, 0 cache write ($0.1923)andPrompt cache (main): 2 requests · 46% of input tokens from cacheObservations from the run, none caused by this PR:
cache_read_input_tokens, nevercache_creation_input_tokens; pre-existingType
🐛 Bug Fix
Caveats (if any)
Low
48hreaches Google as172800s<seconds>sform was never capped before eitherprompt_cache_min_tokenson Gemini 3.x models makes the proxy skip 1024-4095-token blocks Google would cache/v1/messagesis by model namegeminithat routes elsewhere carriescache_controlthrough, the same wayclaudealiases already doci/circleci: litellm_utils_testingis red at the tip ontest_models_by_provider(transcribemissing frommodels_by_provider), a test and module this PR does not touchci/circleci: proxy_e2e_anthropic_messages_testsis red at the tip ontest_bedrock_invoke_messages_with_all_beta_headersfor both Bedrock params, Bedrock answeringinvalid beta flagclaude, so the/v1/messagesgate this PR changes answers the same as before for themgemini-2.5-procost map key carriesdeprecation_date2026-10-20, so its new 2048 minimum matters only until then;gemini/gemini-2.5-prohas no deprecation date<seconds>sttl form for Gemini context caching (docs/providers/gemini.md, "TTL Format Requirements"), so themandhforms need a litellm-docs follow-upclaude, a Bedrock ARN, orgemini(Bedrock Nova model ids, which Converse caches throughcachePoint) still getcache_controlstripped on/v1/messages; pre-existing and untouchedFinal Attestation