feat(prompt-caching): map cache_control_injection_points to OpenAI prompt_cache_breakpoint on GPT-5.6+ targets - #37628
Conversation
…on GPT-5.6+ targets When the resolved deployment is provider openai and the model is GPT-5.6 or newer, the cache control hook now writes prompt_cache_breakpoint on the targeted content block and sets prompt_cache_options to explicit mode unless the caller already passed one. The /v1/messages bridges carry the marker through (the Responses bridge moves a marked system prompt into a developer message, since top-level instructions cannot hold one). Breakpoint counting and the stand-down check recognise both marker kinds, and client breakpoints already present in messages are no longer subtracted from the cap twice. Fixes #37509
|
bugbot run |
Greptile SummaryThe PR maps configured cache-control injection points to OpenAI explicit prompt-cache breakpoints for eligible GPT targets across chat completions, Responses, and Anthropic Messages bridges
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains
|
| Filename | Overview |
|---|---|
| litellm/integrations/anthropic_cache_control_hook.py | Selects the OpenAI caching dialect, injects explicit markers, and coordinates breakpoint limits and request options |
| litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py | Preserves supported prompt-cache markers while converting Anthropic Messages payloads to Responses input |
| litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py | Carries supported prompt-cache markers through the Anthropic-to-chat-completions bridge |
| litellm/responses/utils.py | Retains prompt-cache options and reshapes marked chat-style content into valid Responses input parts |
| litellm/utils.py | Exposes prompt-cache-breakpoint capability through the shared model-support utility |
| model_prices_and_context_window.json | Declares explicit prompt-cache-breakpoint support for the targeted OpenAI GPT models |
Reviews (3): Last reviewed commit: "Fall back to the GPT version rule when t..." | Re-trigger Greptile
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: OpenAI dialect reserves unused tool_config slot
- Gated the tool_config reservation on
not openai_dialectin bothget_chat_completion_promptandapply_to_anthropic_messages_requestso OpenAI targets keep the full 4-block budget, and added a regression test asserting all 4 message markers are emitted alongside a tool_config point.
- Gated the tool_config reservation on
Or push these changes by commenting:
@cursor push bab8efa15b
Preview (bab8efa15b)
diff --git a/litellm/integrations/anthropic_cache_control_hook.py b/litellm/integrations/anthropic_cache_control_hook.py
--- a/litellm/integrations/anthropic_cache_control_hook.py
+++ b/litellm/integrations/anthropic_cache_control_hook.py
@@ -110,14 +110,16 @@
else:
remaining_points.append(point)
+ openai_dialect: Final = AnthropicCacheControlHook._targets_openai_prompt_cache_breakpoint(
+ model, injection_points[0].get("_litellm_provider")
+ )
# Non-message points (currently Bedrock tool_config) are handled in the
# provider transform, where each tool_config point appends at most one
# cachePoint to the tools. That block also counts toward Anthropic's
- # limit, so reserve a slot for it here to leave room.
- reserved_blocks: Final = 1 if any(p.get("location") == "tool_config" for p in remaining_points) else 0
-
- openai_dialect: Final = AnthropicCacheControlHook._targets_openai_prompt_cache_breakpoint(
- model, injection_points[0].get("_litellm_provider")
+ # limit, so reserve a slot for it here to leave room. OpenAI targets
+ # don't consume a tool_config breakpoint (no-op there), so no reserve.
+ reserved_blocks: Final = (
+ 1 if not openai_dialect and any(p.get("location") == "tool_config" for p in remaining_points) else 0
)
breakpoints_before: Final = AnthropicCacheControlHook._count_request_cache_breakpoints(processed_messages)
processed_messages = self._apply_message_injections(
@@ -363,7 +365,9 @@
else:
remaining_points.append(point)
- reserved_blocks: Final = 1 if any(p.get("location") == "tool_config" for p in remaining_points) else 0
+ reserved_blocks: Final = (
+ 1 if not openai_dialect and any(p.get("location") == "tool_config" for p in remaining_points) else 0
+ )
max_blocks: Final = MAX_CACHE_CONTROL_BLOCKS - reserved_blocks
message_blocks: Final = AnthropicCacheControlHook._count_request_cache_breakpoints(processed_messages)
diff --git a/tests/test_litellm/integrations/test_anthropic_cache_control_hook.py b/tests/test_litellm/integrations/test_anthropic_cache_control_hook.py
--- a/tests/test_litellm/integrations/test_anthropic_cache_control_hook.py
+++ b/tests/test_litellm/integrations/test_anthropic_cache_control_hook.py
@@ -1281,6 +1281,48 @@
]
+def test_cache_control_hook_openai_dialect_does_not_reserve_tool_config_slot():
+ """OpenAI targets no-op on tool_config, so the slot must not be reserved.
+
+ A shared deployment config that lists 4 message points plus a tool_config
+ point (for the Bedrock target) must still emit 4 prompt_cache_breakpoint
+ markers on the OpenAI target: the tool_config point is unused there.
+ """
+ hook = AnthropicCacheControlHook()
+
+ messages: List[AllMessageValues] = [{"role": "user", "content": f"turn {i}"} for i in range(4)]
+
+ _, processed, non_default_params = hook.get_chat_completion_prompt(
+ model="openai/gpt-5.6",
+ messages=messages,
+ non_default_params={
+ "cache_control_injection_points": [
+ {"location": "message", "index": 0},
+ {"location": "message", "index": 1},
+ {"location": "message", "index": 2},
+ {"location": "message", "index": 3},
+ {"location": "tool_config"},
+ ]
+ },
+ prompt_id=None,
+ prompt_variables=None,
+ dynamic_callback_params={},
+ )
+
+ marker_count = sum(
+ 1
+ for msg in processed
+ if isinstance(msg.get("content"), list)
+ for block in msg["content"]
+ if isinstance(block, dict) and block.get("prompt_cache_breakpoint") is not None
+ )
+ assert marker_count == 4
+ assert non_default_params["prompt_cache_options"] == {"mode": "explicit"}
+ assert non_default_params["cache_control_injection_points"] == [
+ {"location": "tool_config", "_litellm_judged": True}
+ ]
+
+
@pytest.mark.asyncio
async def test_cache_control_hook_bedrock_payload_caps_with_tool_config_point():
"""End-to-end: message + tool_config injection must not exceed 4 cachePoints."""You can send follow-ups to the cloud agent here.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…m through /v1/responses
The cache control hook also runs on litellm.responses() input. On a
GPT-5.6 deployment it wrapped a string-content item into a chat-shaped
{"type": "text"} part, which the Responses API rejects, and it never
marked input_text, input_image or input_file parts, so no breakpoint and
no prompt_cache_options reached the provider. Add the Responses part
types to the eligible block set and translate chat-shaped text parts on
non-assistant items to input_text in
ResponsesAPIRequestUtils.merge_prompt_management_input, which both the
async and the sync prompt management sites go through.
The dialect also fired for any GPT-5.6 name that resolved to provider
openai, including deployments pointed at a custom api_base that does not
understand prompt_cache_breakpoint. Decide it once per request from the
provider, the model map and the resolved api_base (request, then
litellm.api_base, then OPENAI_BASE_URL / OPENAI_API_BASE): only
api.openai.com and *.api.openai.com hosts speak the dialect, a top-level
prompt_cache_options opts a custom target in, and litellm_proxy/ targets
never get it. maybe_seed_default_injection_points takes api_base and
stamps the finished decision on the points as _litellm_openai_dialect so
the sync completion() path, whose hook params do not carry api_base,
honors it; maybe_inject_cache_control takes api_base from the
/v1/messages handler.
Eligibility now comes from a supports_prompt_cache_breakpoint model map
flag on the OpenAI gpt-5.6 entries, exposed through
litellm.utils.supports_prompt_cache_breakpoint, with the GPT version rule
kept only for models the map does not know. The OpenAI dialect no longer
reserves a slot for tool_config points, which OpenAI has no cache block
for, and with_prompt_cache_breakpoint plus the chat bridge helper return
a new block instead of mutating their input.
|
bugbot run |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: base_url skipped by dialect gate
- Passed the named
base_urlargument as a fallback forapi_basewhen seeding cache injection points in bothcompletionandacompletion, so custom hosts specified via the OpenAI-stylebase_urlalias are now recognized by the dialect gate.
- Passed the named
Or push these changes by commenting:
@cursor push c11c442cb1
Preview (c11c442cb1)
diff --git a/litellm/main.py b/litellm/main.py
--- a/litellm/main.py
+++ b/litellm/main.py
@@ -507,7 +507,7 @@
custom_llm_provider=cast(str | None, custom_llm_provider), # cast-ok: read from untyped kwargs
tools=tools,
enable_prompt_caching=cast(bool | None, kwargs.get("enable_prompt_caching")), # cast-ok: untyped kwargs
- api_base=kwargs.get("api_base"),
+ api_base=kwargs.get("api_base") or base_url,
)
if isinstance(litellm_logging_obj, LiteLLMLoggingObj) and (
@@ -5172,7 +5172,7 @@
custom_llm_provider=cast(str | None, kwargs.get("custom_llm_provider")), # cast-ok: untyped kwargs
tools=tools,
enable_prompt_caching=cast(bool | None, kwargs.get("enable_prompt_caching")), # cast-ok: untyped kwargs
- api_base=kwargs.get("api_base"),
+ api_base=kwargs.get("api_base") or base_url,
)
if isinstance(litellm_logging_obj, LiteLLMLoggingObj) and (You can send follow-ups to the cloud agent here.
…oint flag A proxy on the default remote cost map never produced a prompt cache breakpoint: the published map has the gpt-5.6 entries without supports_prompt_cache_breakpoint, so the model-map gate returned False for every listed model and only LITELLM_LOCAL_MODEL_COST_MAP=True (the repo .env, hence the passing unit tests) made the feature work. The hook now honors the flag when the entry carries one, True or False, and otherwise applies the GPT-5.6+ version rule to the model name, so a map that lags the flag still gets the OpenAI dialect. The model-map tests pin litellm.model_cost to the bundled backup map and a new test drives the hook against an unflagged gpt-5.6 entry. completion() and acompletion() take base_url as an alias for api_base that only lands on api_base after the cache control hook ran, so a GPT-5.6 call at a non-OpenAI gateway given through base_url still got the dialect. Both seed calls and the unstamped request-params read now look at base_url too. ResponsesAPIRequestUtils.merge_prompt_management_input reshaped hook output in place, retyping text parts to input_text on the caller's own message objects. The merge now shapes a copy of each message as it emits it, so the identity-based merge keeps working on the hook's objects and nothing the hook or the client owns is mutated.
|
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 e16cf5e. Configure here.
…cache_breakpoint feat(prompt-caching): map cache_control_injection_points to OpenAI prompt_cache_breakpoint on GPT-5.6+ targets
* Document cache_control_injection_points on OpenAI GPT-5.6 targets Adds the OpenAI explicit breakpoint mapping that BerriAI/litellm#37628 ships: how injection points become prompt_cache_breakpoint markers plus prompt_cache_options, the per-deployment mode and ttl override, the api.openai.com gate with the prompt_cache_options opt-in for custom api_base deployments, the /v1/responses input_text behavior, and the supports_prompt_cache_breakpoint model map flag. * Describe the version-rule fallback and the base_url alias for OpenAI breakpoints

TLDR
Problem this solves:
cache_control_injection_pointsonly emits Anthropiccache_control, which OpenAI stripsprompt_cache_breakpoint) is unreachable from config/v1/messagesand/v1/responsesclients cannot place an OpenAI breakpointHow it solves it:
prompt_cache_breakpointon the configured blockprompt_cache_options: {"mode": "explicit"}, a caller's own value wins/v1/responsesand both/v1/messagesbridgesDesign notes: why, and what changed per file
Why
OpenAI's GPT-5.6+ models support explicit prompt caching through a block-level
prompt_cache_breakpointmarker and a request-levelprompt_cache_optionsobject. LiteLLM'scache_control_injection_pointsonly ever emitted Anthropic'scache_control, which the OpenAI transformation strips before sending, so a proxy that routes the same deployment config to Anthropic and OpenAI models could not place breakpoints on the OpenAI side./v1/messagesclients (Claude Code, the Anthropic SDK) also had no way to get a breakpoint onto an OpenAI request, because the Responses bridge folded the system prompt into top-levelinstructions, which cannot carry one.What changed
litellm/integrations/anthropic_cache_control_hook.pyopenai, and the request must really target OpenAI. Eligibility comes from the model map when the entry carries the flag:supports_prompt_cache_breakpoint: trueis set on the OpenAIgpt-5.6,gpt-5.6-sol,gpt-5.6-terraandgpt-5.6-lunaentries, exposed through a newlitellm.utils.supports_prompt_cache_breakpoint(model, custom_llm_provider=None)built on the same_supports_factoryassupports_prompt_caching, and an explicitfalseis honored. A model the map does not know, or a listed entry without the flag, falls back to thegpt-<major>[.<minor>]version rule (5.6 or newer, sogpt-5.6on a published map that does not carry the flag yet, an unlistedgpt-5.7orgpt-6all qualify whilegpt-4.1oro3do not). The first QA round caught this: a proxy on the default remote cost map never fired because the published map lags this PR, and the unit tests only passed because the repo.envpinsLITELLM_LOCAL_MODEL_COST_MAP=True. The provider is thecustom_llm_providerthe caller gave when there is one, elseget_llm_provider(model); a routing failure falls back to today's behavior andlitellm_proxy/<model>never gets the dialect. The target check resolves the requestapi_base(or itsbase_urlalias, whichcompletion(),acompletion()andlitellm.responses()accept), thenlitellm.api_base, thenOPENAI_BASE_URL/OPENAI_API_BASE: unset,api.openai.comand*.api.openai.comhosts speak the dialect; any other host keeps today's Anthropic-style behavior unless the request carries a top-levelprompt_cache_options, which is the opt-in for an OpenAI-compatible target that understands the dialect.maybe_seed_default_injection_points(which seesapi_baseand the provider, and now takesapi_basefrom bothcompletion()andacompletion()) stamps the finished decision onto the configured points as_litellm_openai_dialect, in the same spirit as the existing_litellm_judgedstamp, so the chat-path hook entry point (which never seesapi_base) honors it on the sync path too; unstamped points are judged from the request params, and/v1/messagespasses itsapi_basetomaybe_inject_cache_control. The stamp is only added for eligible models so every other request keeps its points untouched, and provider resolution is skipped entirely for ineligible models, so unroutable custom model names never trigger theget_llm_providerlookup.[{"type": "text", "text": ...}], a string/v1/messagessystem becomes one text block, and list content gets the marker on the last block OpenAI can carry it on (text,image,image_url,file,input_audio, and the Responses input partsinput_text,input_image,input_file), walking back pasttool_resultand other ineligible blocks. Assistant messages are never marked, and a turn with no eligible block (for example atool_result-only user turn) is left alone and does not consume one of the four slots. The injection point'scontrolfield is ignored, sinceexplicitis the only mode OpenAI accepts.prompt_cache_options = {"mode": "explicit"}on the request (non_default_paramson the chat path, the handler kwargs on/v1/messages) unless the caller already passed one, so a deployment-levelprompt_cache_options: {"mode": "explicit", "ttl": "30m"}inlitellm_paramswins.prompt_cache_breakpointmakes configured points stand down exactly like a client that sendscache_control.apply_to_anthropic_messages_request: client breakpoints already present in the messages were subtracted from the cap once for the system point budget and again inside the message pass, which silently lost one injectable slot per client-marked message. The message pass now receives the cap minus the system markers only.tool_configpoints no longer reserve one of the four slots in the OpenAI dialect, on the chat path and on/v1/messagesalike: OpenAI has no tool-config cache block, so four message points plus atool_configpoint now mark all four messages there. Bedrock targets keep the reservation.litellm/responses/utils.py(/v1/responses)cache_control_injection_pointson the Responses path now shapes chat-style{"type": "text"}parts on non-assistant input items back intoinput_text(the hook wraps a string item into a text block), so a markedsystem,developeroruserinput item is a valid Responses input item.prompt_cache_optionstravels throughResponsesAPIOptionalRequestParamsand reaches the outbound body on bothlitellm.responsesandlitellm.aresponses. The reshape returns copies, so the hook's output and any client message objects a prompt-management hook hands back unchanged are never mutated.litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py(/v1/messagesto Responses API)instructions; it becomes a leadingdevelopermessage with oneinput_textpart per text block, markers preserved. System prompts without a breakpoint still becomeinstructions.prompt_cache_breakpointis carried from usertexttoinput_text, from userimagetoinput_image, and on mid-turn system blocks. Assistant andtool_resultblocks drop it, since OpenAI does not accept breakpoints there.prompt_cache_optionsrides tolitellm.aresponsesthrough the existing extra-kwargs forwarding.litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py(/v1/messagesto chat completions)prompt_cache_breakpointonto the OpenAI text and image blocks at the system and user sites.prompt_cache_optionsreachesacompletionthrough the existing kwargs forwarding and thenextra_body.litellm/types/llms/openai.py,litellm/types/llms/anthropic.py,litellm/types/integrations/anthropic_cache_control_hook.py,litellm/types/utils.py,litellm/utils.py,litellm/litellm_core_utils/prompt_templates/common_utils.py,model_prices_and_context_window.json(and its backup)PromptCacheBreakpointandPromptCacheOptionsTypedDicts,prompt_cache_optionsonResponsesAPIOptionalRequestParamsso the Responses param filter keeps it,prompt_cache_breakpointon the Anthropic text, image and system block types, the_litellm_openai_dialectstamp on the injection point types, asupports_prompt_cache_breakpointcapability flag onProviderSpecificModelInfo/ModelInfo(populated byget_model_infolikesupports_prompt_caching, schema regenerated) set on the four OpenAI GPT-5.6 entries, and a small purewith_prompt_cache_breakpointhelper shared by the hook and the adapters (it returns a new block carrying the marker and callers use the return value; the chat bridge helper does the same).ChatCompletionTextObjectdeliberately does not grow the field: pydantic builds a schema for it at import time and warns about everyReadOnlyitem it sees, while the repo's type-discipline gate requiresReadOnlyon TypedDict fields, so the hook sets the key through the helper instead.Chat completions transformation: no code change.
prompt_cache_optionsis deliberately not added to the supported chat params;get_optional_paramsalready moves unknown params for theopenaiprovider intoextra_body, which is the only way it can reach the installed OpenAI SDK (itschat.completions.createhas no such kwarg). The existingcache_controlstrip leavesprompt_cache_breakpointalone. Tests pin both facts.Tests: new unit coverage (no network) in the existing modules for the hook (dialect table, provider stamp with
custom_llm_providerset toopenai, an OpenAI-compatible provider and unset, block placement and walk-back, assistant andtool_result-only turns skipped without consuming a slot,toolrole text marked on the chat path,controlignored, callerprompt_cache_optionspreserved, stand-down and cap counting across both marker kinds, the double-count fix, non-OpenAI and pre-5.6 targets unchanged with point identity preserved), the Responses bridge (developer message, marker carry, assistant andtool_resultdrop, kwargs forwarding), the OpenAI Responses transformation, the chat transformation, the chat bridge, the/v1/responsesrequest bodies (litellm.responsesandlitellm.aresponseson a GPT-5.6 target, below 5.6, behind a customapi_base, on alitellm_proxy/target, with theprompt_cache_optionsopt-in and on a regional*.api.openai.comhost) and the Responses input shaping, the target gate on the chat path and/v1/messages(customapi_base,litellm.api_baseand env fallbacks, requestapi_baseover env,litellm_proxy/, the opt-in, the stamped decision and its authority over request params, andcompletion()with a customapi_baseagainst a mocked OpenAI client), thetool_configslot in both dialects, and the model-map flag with its version fallback plus the model map schema check, the published-map fallback (model-map tests pinned to the bundled map, the hook run against an unflaggedgpt-5.6entry, and the touched suites run with the remote map),base_urloncompletion(),acompletion()andlitellm.responses(), and the copy-on-reshape behavior of the Responses merge.User Flow
Before: on an OpenAI GPT-5.6 deployment the configured injection point changes nothing. OpenAI's automatic breakpoint decides what gets cached, every turn pays a write for its own tail, and no proxy setting switches the deployment to explicit mode
gpt-5.6deployment (openai/gpt-5.6) withcache_control_injection_points: [{"location": "message", "role": "system"}]to config.yaml and starts the proxysystemprompt and user question A, which carries about 1500 tokens of session notes of its own: 200, andusage.cache_creation_input_tokenscovers the whole prompt including the user turn (7495 in the QA run) whilecache_read_input_tokensis 0cache_read_input_tokens: 6002andcache_creation_input_tokens: 1479, so the system prompt was read and the whole new user turn was written again at OpenAI's automatic breakpointcache_read_input_tokens: 6002and another 1514 tokens written. Nothing about the configured system breakpoint shows up, and no config setting puts the deployment in explicit mode or sets a ttlsystemplus ausermessage):usage.prompt_tokens_detailsfollows the same automatic pattern, 7537 written, then 1494 and 1478 on top of 6001 readsysteminput item plus the user item): same automatic pattern (7500, then 1513 and 1524), and a developer who puts their ownprompt_cache_breakpointon the system item andprompt_cache_options: {"mode": "explicit"}on the request sees both silently dropped: 7497 written, then 1517 and 1505After: the same injection point becomes an OpenAI
prompt_cache_breakpointand the request runs in explicit mode, so the cache boundary sits exactly at the configured system prompt on all three endpoints and the follow-up turns write nothinggpt-5.6deployment (openai/gpt-5.6) withcache_control_injection_points: [{"location": "message", "role": "system"}]to config.yaml and starts the proxysystemprompt and user question A, which carries about 1500 tokens of session notes of its own: 200, andusage.cache_creation_input_tokensnow covers only the system prompt (6001) whilecache_read_input_tokensis 0 and the user turn is plaininput_tokenscache_read_input_tokens: 6001and nocache_creation_input_tokensat all: nothing past the configured breakpoint is writtencache_read_input_tokens: 6001again. The admin can addprompt_cache_options: {"mode": "explicit", "ttl": "30m"}to the deployment'slitellm_paramsand it is honoredusage.prompt_tokens_details.cache_write_tokensis 6001, then 0 and 0, withcached_tokens: 6001on calls 2 and 3systeminput item:cache_write_tokens6002, then 0 and 0, and the developer's ownprompt_cache_breakpointplusprompt_cache_optionson a deployment with no injection point is forwarded instead of dropped (6001 written, then 0 and 0)Relevant issues
Fixes #37509
Related: #32656
Linear ticket
Resolves LIT-5876
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 drive the same client scripts, unmodified, against a proxy booted from the named commit with this config (
__MASTER_KEY__differs per leg).gpt-5.6carries the injection point from the ticket,gpt-5.6-plainhas none and is used for the client-sent-marker case.Every case sends the same ~6k-token reference text as the system prompt (4598 words, plus a per-run
Session salt: <hex>line so each run starts from a cold cache), then three calls 6 s apart: question A, question B, question B again. Each user message carries its own ~1500-token block of scratch notes seeded from the salt and the call index, the way a real session's turns differ after the shared prefix. That tail is what tells the two modes apart in the usage numbers: OpenAI's implicit caching caches the whole prompt, so every call writes its new tail on top of the reads, while an explicit breakpoint on the system message caches up to the marker and nothing after it, so call 1 writes ~6000 and calls 2 and 3 write 0. The helper shared by the scripts:common.py
case_messages.py (Anthropic SDK, the ticket's client)
case_chat.py (OpenAI SDK)
case_responses.py (OpenAI SDK)
case_responses_client_explicit.py (OpenAI SDK, client places its own marker and options, run against gpt-5.6-plain)
Before (6fcdea0)
/v1/messages (Anthropic SDK)
python case_messages.py --port 46429 --key sk-qa-lit5876-before --salt 668d76e2/v1/chat/completions (OpenAI SDK)
python case_chat.py --port 46429 --key sk-qa-lit5876-before --salt 783c4435cache_write_tokens7537 then 1494 then 1478/v1/responses (OpenAI SDK)
python case_responses.py --port 46429 --key sk-qa-lit5876-before --salt a169348bcache_write_tokens7500 then 1513 then 1524/v1/responses with a client-sent prompt_cache_breakpoint and prompt_cache_options (gpt-5.6-plain)
python case_responses_client_explicit.py --model gpt-5.6-plain --port 46429 --key sk-qa-lit5876-before --salt 443c99b2prompt_cache_optionsnever reached OpenAIClaude Code TUI on the gpt-5.6 deployment
tmux new-session -d -s lit5876qa_before -x 180 -y 45 "env ANTHROPIC_BASE_URL=http://127.0.0.1:46429 ANTHROPIC_AUTH_TOKEN=sk-qa-lit5876-before ANTHROPIC_MODEL=gpt-5.6 ANTHROPIC_SMALL_FAST_MODEL=gpt-5.6 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 claude", accept the trust prompt, thentmux send-keys "Reply with exactly the word pong and nothing else" Entercache_controlmarkers, the session works and answers)After (e16cf5e)
/v1/messages (Anthropic SDK)
python case_messages.py --port 29616 --key sk-qa-lit5876-after --salt 0841ef16input_tokensinstead of being rewritten on every call/v1/chat/completions (OpenAI SDK)
python case_chat.py --port 29616 --key sk-qa-lit5876-after --salt 9775ffc7cache_write_tokens6001 then 0 then 0 withcached_tokens6001 on the follow-ups/v1/responses (OpenAI SDK)
python case_responses.py --port 29616 --key sk-qa-lit5876-after --salt 9aaecf74cache_write_tokens6002 then 0 then 0/v1/responses with a client-sent prompt_cache_breakpoint and prompt_cache_options (gpt-5.6-plain)
python case_responses_client_explicit.py --model gpt-5.6-plain --port 29616 --key sk-qa-lit5876-after --salt 543756c9prompt_cache_optionsnow reach OpenAI on a deployment with no injection point, so it gets the explicit pattern it asked forClaude Code TUI on the gpt-5.6 deployment
tmux new-session -d -s lit5876qa_after -x 180 -y 45 "env ANTHROPIC_BASE_URL=http://127.0.0.1:29616 ANTHROPIC_AUTH_TOKEN=sk-qa-lit5876-after ANTHROPIC_MODEL=gpt-5.6 ANTHROPIC_SMALL_FAST_MODEL=gpt-5.6 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 claude", thentmux send-keys "Reply with exactly the word pong and nothing else" Entercache_controlmarkers, the configured injection point stands down as before, and the session works the same wayBoth proxies ran on the default remote cost map (no
LITELLM_LOCAL_MODEL_COST_MAPin either environment), which is what caught the first round: the published map has nosupports_prompt_cache_breakpointongpt-5.6yet, so the version-rule fallback in e16cf5e is what makes the After leg fire.Closing observations from the run:
input_tokensnow count the uncached tail; PR causes, correctprompt_cache_optionson/v1/responseswas dropped at base; PR fixescache_controlstill stands the point down; PR leaves aloneinstructionsstring stays on implicit caching; PR leaves alonecache_creation_input_tokensfor OpenAI writes; PR leaves aloneType
🆕 New Feature
Caveats (if any)
openaiis covered;azurekeeps the Anthropic-style behavior for now. A GPT-5.6+ deployment pointed at a customapi_base(requestapi_baseorbase_url,litellm.api_base, orOPENAI_BASE_URL/OPENAI_API_BASE) keeps the previous behavior unless the deployment setsprompt_cache_options, which opts it into the dialect.litellm_proxy/targets never get the dialect: the downstream LiteLLM that fronts OpenAI decides, and once it runs this change it produces the markers itself. Fine-tune ids (ft:gpt-5.6:...) are not in the model map and do not match the version rule, so they keep today's behavior.tool_resultblocks cannot carry a breakpoint on OpenAI, so the hook never marks an assistant message and skips turns with no eligible block; an injection point that only resolves to such turns is a no-op on OpenAI targets andprompt_cache_optionsis left unset.tool_configpoints are likewise a no-op there.cache_control, so its requests through/v1/messagesare not re-marked.prompt_cache_optionsis anextra_bodypassthrough. A clientprompt_cache_optionsnested inside its ownextra_bodyis invisible to the hook and gets overwritten by the hook's explicit default during theextra_bodymerge; pass it top-level (or in the deployment'slitellm_params) to control mode and ttl./v1/responsesa top-levelinstructionsstring cannot carry a breakpoint, so an injection point only markssystem,developeranduserinput items; a caller who wants the system prompt behind an explicit breakpoint sends it as an input item rather than asinstructions.prompt_cache_options) rather than first-time cache hits.Live PR risk (base 6fcdea0 vs head e16cf5e)
A three-proxy rig ran the same client flows against a base upstream and a head upstream, both fronting a downstream LiteLLM at base that fronts OpenAI, with
drop_paramsoff everywhere.litellm_proxy/gpt-5.6through the downstream, a baregpt-5.6withapi_baseat the downstream, Gemini and xAI through that same chain, Claude on/v1/messageswith five marked blocks, Bedrock tool calls, agpt-5.6deployment with atool_configpoint, streaming chat,/model/infoand/health/readinessgpt-5.6deployment that setsprompt_cache_optionsinlitellm_paramsand pointsapi_baseat another gateway went from no caching at all at base (explicit options with no marker: 0 written and 0 read on all three calls) to the explicit pattern at head (6001 written, then 0 written and 6001 read through both hops). That is the opt-in the Limitations describe and the only flow outside the marked deployments whose numbers change. On/v1/messagesthe uncached tail now shows asinput_tokensinstead of being counted as a writelitellm_proxy/and customapi_basenever get the dialect), a client that sends its owncache_controlongpt-5.6still stands the injection point down, a Responses request with the system prompt ininstructionsstays on implicit caching with no options forced,/model/infois identical between sides apart from ids, and the spend row is recorded on bothanthropic_cache_control_hook.py) to the seed calls incompletion(),acompletion()andlitellm.responses(), the Responses merge inresponses/utils.py, the OpenAI chatextra_bodypassthrough and the Anthropic messages adapter. The e16cf5e additions got their own walk: the version rule only runs when the map entry lacks the flag,base_urlfeeds the same gateapi_basedid, and the copy-on-reshape touches only the Responses merge outputFinal Attestation
Note
Cursor Bugbot is generating a summary for commit 5f6d22e. Configure here.