Repository navigation
fix(responses): keep prompt_cache_breakpoint markers in the chat to responses bridge - #44119
Conversation
…esponses bridge Preserve cache-breakpoint markers through the bridge for supported models and drop them for models without breakpoint support Co-authored-by: Simon Sorg <simonsorg13@gmail.com> Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
bugbot run |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
…llers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
|
@greptileai please re-review the current head ccf03ee, the converter now strips markers for non-bridge callers |
…nt blocks The marker read ran every chat content block through TypeAdapter(dict[str, object]).validate_python, which rejects dict blocks with non-string keys that chat completion callers passing Python dicts could previously send; the request then failed with a pydantic ValidationError on the bridge keep path, the strip path, and the image/file conversions alike. item is already isinstance-narrowed to a dict at every read site, so read the marker with dict.get directly and drop the adapter. Adds a regression test covering text/image_url/file blocks carrying non-string keys on both the keep (gpt-5.6) and strip (gpt-4o) paths.
|
bugbot run |
…oint basedpyright flags the raw dict.get read as reportUnknownArgumentType (+2 against the error budget); cast the isinstance-narrowed block to dict[str, object] first, matching the strip helpers' cast-ok idiom.
A Final assignment inside the conversion loop trips reportGeneralTypeIssues, and per-site casts trip the LIT006 budget; read the marker through one cast-narrowed local instead.
|
bugbot run |
|
bugbot run |
|
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 f4042d8. Configure here.
…breakpoint an input item Since this branch was cut, main carries prompt_cache_breakpoint markers through the bridge on input items' content blocks (BerriAI#42281, BerriAI#44119, BerriAI#45500), and its tests expect a leading system message whose block carries one to stay an input item so the marker survives. Folding that message into `instructions` dropped the marker and broke eight of those tests. The fold now skips a leading system message any of whose blocks carries a prompt_cache_breakpoint, alongside the non-text case it already skipped. A block with only cache_control, the shape BerriAI#42171 reports, still folds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TLDR
Problem this solves:
prompt_cache_breakpointmarker on text, image and file blocksHow it solves it:
base_modelsupports breakpointsprompt_cache_options: {"mode": "explicit"}unchangedThis is a copy of #42281 without its default flip from
explicittoimplicit. That flip also changed plain/v1/chat/completionsand direct/v1/responses, left/v1/messagesonexplicit, and contradicted the documented default, so it needs its own decisionUser Flow
Before: an agent app running a tool loop on gpt-5.6 pays full price for its long system prompt on every turn
tools, a 3k-token system prompt and the growing tool historyusage.prompt_tokens_details.cached_tokens: 0prompt_cache_breakpointgets the same 0, since the markers never reach OpenAIAfter: the same app reads the system prompt from cache on every turn after the first
toolsand the growing historyPre-Submission checklist
Screenshots / Proof of Fix
Two proxies with 2 workers each, real Postgres, real OpenAI, Azure OpenAI and OpenRouter, no mocks. The base runs the merge base, the head runs the PR tip, same config. Every prompt starts with a per-leg random nonce so the legs never share the provider cache. What the provider received was read from the
POST Request Sent from LiteLLMblock of the proxy logBefore (aa601ce)
Tool loop on /v1/chat/completions (bridged to /v1/responses)
toolsand a growing history: cached tokens 0, 0, 0, 0 and cache writes 0, 0, 0, 0prompt_cache_options: {'mode': 'explicit'}sent with noprompt_cache_breakpointanywhere ininputImage block with a caller marker on the bridge
Caller markers on models without breakpoint support
azure/and the/openai/v1api_base): bridge returns 200, markers silently droppedprompt_cache_breakpoint is not supported on this modelAfter (ccf03ee)
Tool loop on /v1/chat/completions (bridged to /v1/responses)
prompt_cache_options: {'mode': 'explicit'}plusprompt_cache_breakpointon the markedinput_textblocksImage block with a caller marker on the bridge
input_imageCaller markers on models without breakpoint support
azure/and the/openai/v1api_base): bridge still returns 200, wire has noprompt_cache_breakpointUnchanged paths
mode: explicitprompt_cache_options: {"mode": "explicit", "ttl": "30m"}goes out unchanged on both legs/v1/responseson gpt-5.6: samemode: explicitand markers on both legs/v1/messageson gpt-5.6 uses its own Messages to Responses adapter, not this bridge. Deployment injection points sendmode: explicitand two markers on both legs, client-onlycache_controlsends neither on both legs (LIT-9162)api_base: 200 on both legs, head now forwards the caller markers and OpenRouter accepts themCheckpoint cells (same two legs)
1,[1],""or a 5120-character string on gpt-5.6: base bridge 200 with the marker dropped, head bridge forwards it and OpenAI returns 400, the same 400 native chat returns on both legs. A valid marker on two blocks reaches the wire twice on headfoundry-aliaswithbase_model: gpt-5.6: head forwards the marker, base drops it. Alias with an unsupportedbase_model: no marker on both legs/v1/responseswith a content-rewriting guardrail: text masked and explicit marker kept on both legsLocal providers suite
tests/integration/run.py providerswith the CircleCI rig recipe, seeds 4106601: head 315 passed, 4 failed. Base 308 passed, 11 failed. The 4 head failures fail on base with the same owned-Redis readiness assertionThe four CircleCI reds with no main reproduction (unit
print_verbose, retained OpenAI clients, Noma v2 timeout, Javelin timeout) and the TPM routing test each pass locally on both legsDeterministic integration audit
Scripted upstream, real proxy with 2 workers per leg, real Postgres and Redis, no litellm component mocked. Same command on both legs:
uv run --no-sync python tests/integration/run.py providers --seed 4106601 --order-seed 4242overtests/integration/providers/test_responses_bridge_prompt_cache_breakpoint.pyandtests/integration/providers/test_responses_bridge_prompt_cache_breakpoint_chaos.pyintegration-providers(job 2235383). The job is red on one unrelated test,test_concurrent_team_model_tpm_requests_reserve_tokens_before_reaching_the_provider, which also fails on maintest_caller_prompt_cache_breakpoints_survive_chat_to_responses_bridge[False-openai_sync],[False-openai_async],[False-httpx]test_caller_prompt_cache_breakpoints_survive_chat_to_responses_bridge[True-openai_sync],[True-openai_async],[True-httpx]cache_control_injection_points, no optionstest_deployment_cache_control_injection_without_options_is_unchangedprompt_cache_optionsimplicit ttl 30mtest_deployment_prompt_cache_options_override_is_unchanged/v1/responsesmarkertest_unmarked_bridge_and_direct_responses_marker_are_forwarded_unchangedtest_unsupported_model_drops_breakpoints_without_rejecting_the_request[False],[True]test_responses_bridge_prompt_cache_breakpoint_chaos.py::test_worker_and_peer_outages_preserve_markers_and_recoverType
🐛 Bug Fix
✅ Test
Caveats (if any)
Medium
prompt_cache_breakpointto OpenAI-compatible backends whose model name passes the support checkapi_basebackends were not runbase_modelset, or markers are droppedLow
/v1/chat/completionsstill returns 400 for caller markers on unsupported models, unchanged from base (LIT-9161)/v1/responsesguardrail rewritebase_modelalias, malformed marker valuesFinal Attestation
ran /live-pr-risk and !audit at ccf03ee: results above
REVIEWER MUST KNOW BEFORE APPROVING
base_modelthat is) now receive caller and injectedprompt_cache_breakpointfields on bridged/v1/chat/completionsrequests, a new field on the provider wire, so cache writes and reads now show up where they were 0mode: explicit. The fix(responses): preserve prompt cache reuse in chat bridge #42281 flip toimplicitis intentionally not includedLink to Devin session: https://app.devin.ai/sessions/fa4f9e145d28404ca20bef7772ba7011
Open in Devin Desktop: https://app.devin.ai/desktop/session/fa4f9e145d28404ca20bef7772ba7011?variant=devin
Requested by: @yucheng-berri
Note
Medium Risk
Changes bridged
/v1/chat/completionsprovider payloads and prompt-cache behavior for supported models; unsupported models rely on correct model/base_model detection to avoid provider 400s.Overview
Fixes the chat → Responses API bridge stripping caller
prompt_cache_breakpointmarkers on text, image, and file blocks (and tool output), which blocked prompt-cache reads on tool-loop traffic.transform_requestnow keeps those markers on the convertedinputwhensupports_openai_prompt_cache_breakpointpasses for the model orlitellm_params.base_model; otherwise it strips them before the upstream call so unsupported models still return 200.convert_chat_completion_messages_to_responses_apigainskeep_prompt_cache_breakpoints(default off) so other callers of the converter still drop markers unless they opt in.Adds unit coverage for keep/strip, alias
base_model, and tool output; plus integration tests (including chaos/worker-kill) that assert upstream/v1/responsespayloads and spend logging.Reviewed by Cursor Bugbot for commit f4042d8. Bugbot is set up for automated code reviews on this repo. Configure here.