fix(anthropic): map metadata.user_id to prompt_cache_key on the /v1/messages bridge - #37623
Conversation
…essages bridge Both /v1/messages bridges (Responses API adapter for openai/* and the chat-completions adapter) now derive prompt_cache_key from the first 64 characters of metadata.user_id, next to the existing user mapping. The chat bridge only sets it when the resolved provider advertises prompt_cache_key in its supported params, so providers that reject unknown params are unaffected. A prompt_cache_key sent explicitly by the client always wins over the derived value. Fixes #37508
Greptile SummaryThe PR derives an OpenAI-compatible
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains within the eligible follow-up scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/experimental_pass_through/adapters/handler.py | Passes the resolved provider into translation and restores an explicit client cache key after translated parameters are merged. |
| litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py | Derives the cache key only when the resolved provider advertises support, excluding chained LiteLLM proxies with unknown backends. |
| litellm/llms/anthropic/experimental_pass_through/responses_adapters/handler.py | Preserves an explicit prompt_cache_key when constructing Responses API arguments. |
| litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py | Maps Anthropic metadata.user_id into the Responses API user and prompt_cache_key fields. |
| litellm/llms/anthropic/experimental_pass_through/utils.py | Adds the shared conversion that stringifies non-null user IDs, truncates them to 64 characters, and omits empty results. |
| litellm/types/llms/openai.py | Extends the chat-completion request type to represent the cache key assigned by the bridge. |
Reviews (2): Last reviewed commit: "fix(anthropic): skip the derived prompt_..." | Re-trigger Greptile
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 cc2013e. Configure here.
|
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 8e1c8c1. Configure here.
TLDR
Problem this solves:
/v1/messagesmapsmetadata.user_idto OpenAI'suserbut never toprompt_cache_keyuser_idis longer than OpenAI's 64-char key limit anywayHow it solves it:
prompt_cache_keyto the first 64 chars ofuser_idprompt_cache_keythe client sends itself still wins over the derived onelitellm_proxy/deployments are skipped: the downstream proxy's real provider is unknown and rejects the key unless it runs withdrop_paramsUser Flow
Before: the request reaches OpenAI with
userfilled in but noprompt_cache_key, so GPT-5.6's cache routing never gets the per-session key the client is already sendingopenai/gpt-5.6-lunato the proxy config undermodel_name: gpt-5.6-lunaand starts the proxyANTHROPIC_BASE_URL=https://litellm-domain,ANTHROPIC_AUTH_TOKEN=<virtual key>,ANTHROPIC_MODEL=gpt-5.6-luna) and types a promptPOST https://litellm-domain/v1/messageswithmodel: gpt-5.6-luna, its system prompt, the message, andmetadata.user_idset to its 150-character session identity ({"device_id":"8c33...","account_uuid":"...","session_id":"..."}), byte-identical on every turn of the session200comes back in Anthropic shape:idstarts withresp_, the answer text, andusage.cache_creation_input_tokenson the first turnhttps://platform.openai.com/logs): the request carriesuserset to the first 64 characters of theuser_idand noprompt_cache_keyat all; the response object echoes"prompt_cache_key": null--detailed_debugoutput: thePOST Request Sent from LiteLLMbody going tohttps://api.openai.com/v1/responseshasuserand noprompt_cache_keylitellm_settings.use_chat_completions_url_for_anthropic_messages: true, the samePOST https://litellm-domain/v1/messagesgoes out asPOST https://api.openai.com/v1/chat/completionswithuserset and, again, noprompt_cache_keyAfter: the same request leaves the gateway with
prompt_cache_keyset to the first 64 characters ofmetadata.user_id, and OpenAI echoes it backopenai/gpt-5.6-lunato the proxy config undermodel_name: gpt-5.6-lunaand starts the proxyANTHROPIC_BASE_URL=https://litellm-domain,ANTHROPIC_AUTH_TOKEN=<virtual key>,ANTHROPIC_MODEL=gpt-5.6-luna) and types a promptPOST https://litellm-domain/v1/messageswithmodel: gpt-5.6-luna, its system prompt, the message, andmetadata.user_idset to its 150-character session identity ({"device_id":"8c33...","account_uuid":"...","session_id":"..."}), byte-identical on every turn of the session200comes back in Anthropic shape:idstarts withresp_, the answer text, andusage.cache_creation_input_tokenson the first turnhttps://platform.openai.com/logs): the request carriesuserandprompt_cache_key, both set to the same first 64 characters of theuser_id({"device_id":"8c33...); the response object echoes"prompt_cache_key": "{\"device_id\":\"8c33..."--detailed_debugoutput: thePOST Request Sent from LiteLLMbody going tohttps://api.openai.com/v1/responseshas bothuserandprompt_cache_keylitellm_settings.use_chat_completions_url_for_anthropic_messages: true, the samePOST https://litellm-domain/v1/messagesgoes out asPOST https://api.openai.com/v1/chat/completionswith bothuserandprompt_cache_keysetprompt_cache_keyin thePOST https://litellm-domain/v1/messagesbody sees that exact value in the upstream request, not the derived oneRelevant issues
Fixes #37508
Linear ticket
Resolves LIT-5875
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
Live before/after QA, no mocks, real calls to
openai/gpt-5.6-luna. Before is a proxy booted from the merge base6fcdea03b0, After a proxy booted from this PR's tip8e1c8c1c62. Each side ran two proxies from its commit on random free ports with--detailed_debug: proxy A with the default Responses-API bridge (config_responses.yaml), proxy C with the chat-completions bridge (config_chat.yaml)config_responses.yaml:config_chat.yamlis the same file plus:Clients: Anthropic Python SDK 0.125.0 pointed at the proxy, OpenAI Python SDK 3.3.1 to retrieve the stored upstream objects the way a developer checks what OpenAI received, Claude Code v2.1.237 driven in tmux. Every case sends a ~2860-token system prompt with a fresh nonce so OpenAI starts cold, then the same
metadata.user_idon both calls of a pair, 3 s apart. The SDK pair used by cases 1, 2, 4 and 5 (case 2 addsextra_body={"store": True}so the completions can be retrieved, case 4 addsextra_body={"prompt_cache_key": ...}, case 5 addsthinking={"type": "enabled", "budget_tokens": 1024}):The
idthe Responses bridge returns is a 241-char wrapper around the upstreamresp_id, which OpenAI's retrieve endpoint rejects (string_above_max_length), so the retrievals below use the upstream id decoded out of it. OpenAI's stored chat object (chat.completions.retrieve) exposesinput_userand noprompt_cache_keyfield on either side, so case 2 shows the request body the gateway sent upstream as read from the admin's--detailed_debuglog (POST Request Sent from LiteLLM) plus the raw usage OpenAI returned. Read the cache numbers honestly:cached_tokensrose on the second call on both sides because OpenAI also caches by prefix on a quiet account, so what this PR changes is the routing key OpenAI stores with the request, which is what keeps one user's hits together under loadSurprises seen during QA, none of them a failure:
idis a 241-char wrapper, OpenAI retrieve rejects it. PR leaves it aloneGET /v1/responses/<id>returns a differently encoded id. PR leaves it alonecache_write_tokens0, live responses showed thousands. PR leaves it aloneinput_user, noprompt_cache_key, nullprompt_tokens_details. PR leaves it alonechatcmpl-<uuid>, hides upstreamresp_id. PR leaves it alonebudget_tokens: 1024becomesreasoning.effort: lowupstream. PR leaves it aloneuserand key cut mid-JSON at 64 chars. PR causes the key half, by design (OpenAI limit)store: trueinextra_bodyis forwarded upstream. PR leaves it alonehttps://api.openai.com/v1/. PR leaves it alone/v1/modelslists 35 (DB-stored models). PR leaves it alone.env,DATABASE_URLsurvivesenv -u. PR leaves it aloneBefore (6fcdea0)
Case 1: Anthropic SDK, Responses-API bridge (proxy A)
user_id = lit5875-qa-before-2a635bbf-e707-4a3f-8d39-56e5641ee726, then retrieve both upstream ids with the OpenAI SDK and the second bridged id through the gateway itselfusage.cache_creation_input_tokens: 2861on call 1 andusage.cache_read_input_tokens: 2861on call 2 (OpenAI caches by prefix even without a key)Case 2: Anthropic SDK, chat-completions bridge (proxy C)
user_id = lit5875-qa-before-7b40ece8-d047-40f8-9ea2-3868fa0837b2andextra_body={"store": True}, then read the twoPOST Request Sent from LiteLLMbodies and the raw usage from the proxy's--detailed_debuglogCase 3: Claude Code against proxy A
ANTHROPIC_BASE_URL=http://127.0.0.1:32698,ANTHROPIC_AUTH_TOKEN=<master key>,ANTHROPIC_MODEL=gpt-5.6-luna, sendReply with the single word ok.thenNow reply with the word again., then retrieve the two prompt requests' upstream ids (the title side request is omitted)useris the first 64 chars of the 150-charuser_id, no key):Case 4: explicit key wins (proxy A)
metadata={"user_id": user_id}andextra_body={"prompt_cache_key": "explicit-lit5875-qa2-before"}, then retrieve the upstream idCase 5: chat bridge with thinking enabled (proxy C)
metadata={"user_id": user_id}andthinking={"type": "enabled", "budget_tokens": 1024}(the chat bridge reroutes this to/v1/responses), then retrieve the upstream idAfter (8e1c8c1)
Case 1: Anthropic SDK, Responses-API bridge (proxy A)
user_id = lit5875-qa-after-90a0a703-2528-46f9-ab3e-811ba4c5fd9c, then retrieve both upstream ids with the OpenAI SDK and the second bridged id through the gateway itselfusage.cache_creation_input_tokens: 2863on call 1 andusage.cache_read_input_tokens: 2863on call 2Case 2: Anthropic SDK, chat-completions bridge (proxy C)
user_id = lit5875-qa-after-a95c5d9f-b481-410f-8a7b-ff7a571f533candextra_body={"store": True}, then read the twoPOST Request Sent from LiteLLMbodies and the raw usage from the proxy's--detailed_debuglogCase 3: Claude Code against proxy A
ANTHROPIC_BASE_URL=http://127.0.0.1:35912,ANTHROPIC_AUTH_TOKEN=<master key>,ANTHROPIC_MODEL=gpt-5.6-luna, sendReply with the single word ok.thenNow reply with the word again., then retrieve the two prompt requests' upstream ids (the title side request is omitted)userandprompt_cache_keyare both the first 64 chars of the 150-charuser_id):Case 4: explicit key wins (proxy A)
metadata={"user_id": user_id}andextra_body={"prompt_cache_key": "explicit-lit5875-after"}, then retrieve the upstream idCase 5: chat bridge with thinking enabled (proxy C)
metadata={"user_id": user_id}andthinking={"type": "enabled", "budget_tokens": 1024}(the chat bridge reroutes this to/v1/responses), then retrieve the upstream idType
🐛 Bug Fix
Caveats (if any)
prompt_cache_key(azure, groq, deepseek, openrouter, vllm, ...) now gets it implicitly on/v1/messages, and there is no config switch to turn the derivation offlitellm_proxy/gets no derived keysession_idfalls past the 64-char cutFinal Attestation
Note
Cursor Bugbot is generating a summary for commit cc2013e. Configure here.