Skip to content

🔖 feat: Anchor GPT-5.6 Cache Breakpoint to the Stable Prefix - #546

Open
berry-13 wants to merge 1 commit into
mainfrom
feat/openai-explicit-prompt-cache-boundary
Open

berry-13 wants to merge 1 commit into
mainfrom
feat/openai-explicit-prompt-cache-boundary

Conversation

@berry-13

Copy link
Copy Markdown
Member

What breaks today

promptCacheExplicit turns on GPT-5.6 explicit caching: applyManagedRequestParams sends prompt_cache_options, and addChatCacheBreakpoints / addResponseCacheBreakpoints stamp prompt_cache_breakpoint on the last system/developer message plus the last cacheable message before the latest user turn.

That first breakpoint is the cross-conversation one, and today it lands in the wrong place. AgentContext keeps instructions (stable) and additional_instructions (dynamic) apart the whole way down — right up to buildSystemMessage, which for every provider except Anthropic, OpenRouter and Bedrock ends at:

return new SystemMessage(
  [stableInstructions, dynamicInstructions].filter((part) => part !== '').join('\n\n')
);

OpenAI and Azure fall through to that line. One system message, stable prefix welded to the dynamic tail, and the breakpoint marks the end of the whole thing. Since the tail is where per-turn content lives — timestamps, memory, retrieved file context, runtime additions — the marked prefix changes on nearly every request. It is written and never read.

Enabling promptCacheExplicit therefore costs a cache write per turn and buys no cross-conversation reuse, while prompt_cache_options: { mode: 'explicit' } has already opted the request out of the implicit latest-message breakpoint it would otherwise have had.

After this change

The dynamic tail moves out of the system message and in behind the stable prefix, so the breakpoint the client places marks content that actually recurs:

SystemMessage  <stable instructions>        <- prompt_cache_breakpoint
HumanMessage   Hello
AIMessage      Hi
HumanMessage   <dynamic instructions>
HumanMessage   Second                       <- second breakpoint sits before this

This is the relocation Anthropic and OpenRouter already perform (shouldMoveDynamicInstructionsbuildPromptCacheDynamicTailbuildBodyWithPromptCacheDynamicTail). The OpenAI path reuses all of it and adds nothing new structurally.

What it deliberately does not reuse is the marker stamping. getPromptCacheProvider() still returns undefined for OpenAI and Azure, so addStablePromptCacheMarkers, addTailCacheControl and the cache_control system blocks stay off: those emit Anthropic-format fields, and OpenAI has no such field. The GPT-5.6 breakpoints are attached later, to the serialized request, inside LibreChatOpenAICompletions / LibreChatOpenAIResponses.

Nothing changes unless promptCacheExplicit === true on an OpenAI or Azure client. Anthropic, OpenRouter, Bedrock and every default-configured OpenAI request keep their existing message shape.

Mechanism

The two concerns that were previously fused under one flag are now separated:

relocate the dynamic tail stamp content markers
Anthropic yes cache_control
OpenRouter yes cache_control
Bedrock no cachePoint
OpenAI / Azure + promptCacheExplicit yes none — applied to the request
buildSystemRunnable
├─ promptCacheProvider        = getPromptCacheProvider()          // marker format
├─ openAIExplicitCache        = usesOpenAIExplicitPromptCache()   // new
├─ splitsDynamicInstructions  = promptCacheProvider != null || openAIExplicitCache
│
├─ buildSystemMessage(...)                  -> stable-only when the tail moved
├─ buildPromptCacheDynamicTail(...)         -> gated on splitsDynamicInstructions
└─ buildBodyWithPromptCacheDynamicTail(...) -> markers only when promptCacheProvider != null

Four call sites:

  • usesOpenAIExplicitPromptCache() — new private predicate: provider is OPENAI or AZURE and clientOptions.promptCacheExplicit === true.
  • buildPromptCacheDynamicTail — takes splitsDynamicInstructions instead of promptCacheProvider; it only ever used that argument as a boolean.
  • buildBodyWithPromptCacheDynamicTail — applies addStablePromptCacheMarkers only when promptCacheProvider != null, leaving the OpenAI prefix untouched.
  • buildSystemMessage — the fallback drops dynamicInstructions when shouldMoveDynamicInstructions is set, so relocated content is not also duplicated in the system message. That flag is false at this line for every pre-existing provider, so the fallback is unchanged for them.

The summary-carrier branch follows the same split: hasSummaryBody && promptCacheProvider == null becomes hasSummaryBody && !splitsDynamicInstructions, so the summary rides the relocated tail rather than being prepended twice.

Tests

src/agents/__tests__/AgentContext.test.ts:

  • moves the dynamic tail behind stable history for openAI/azureOpenAI explicit caching — asserts the system message holds only the stable prefix, the tail sits before the final human turn, and no cache_control appears on any message.
  • keeps dynamic-only instructions in the system message under explicit caching — guards the new shouldMoveDynamicInstructions fallback against dropping instructions when there is no stable prefix to anchor.

npx jest src/agents src/graphs src/summarization → 432 passed. tsc -p tsconfig.build.json clean. The three failing suites in a full run (src/llm/{anthropic,google,vertexai}/llm.spec.ts) are live-API tests that fail identically on a clean checkout.

Downstream

This unblocks the explicit-breakpoint half of danny-avila/LibreChat#14949. LibreChat lands the request-level work separately (deterministic prompt_cache_key, prompt_cache_retention, and the promptCacheExplicit lever itself, default off); with this change it can enable that lever without the breakpoint being spent on volatile content.

`promptCacheExplicit` had no effect on where the breakpoint landed, because
`buildSystemMessage` collapsed the stable instructions and the dynamic tail
into one string for OpenAI and Azure. The OpenAI client marks the last
system/developer message, so the breakpoint sat behind per-turn content and
the cached prefix was invalidated every turn.

The dynamic tail now moves behind the stable prefix, reusing the relocation
Anthropic and OpenRouter already use, while leaving `promptCacheProvider`
undefined so no `cache_control` or `cachePoint` marker is stamped: the OpenAI
breakpoints are attached to the serialized request, not the message content.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T09:16:16.734504Z e3fe253 PR opened
🔒 Security Review Completed 2026-09-15T09:15:43.664358Z e3fe253 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

const dynamicTail = shouldMoveDynamicInstructions
? [new HumanMessage(dynamicInstructions)]
: [];

P1 Badge Keep the dynamic tail in an instruction role

When explicit caching is enabled and both instruction strings are nonempty, this converts the entire dynamic system tail into a HumanMessage. That tail includes host-supplied additional_instructions and cross-run summary context; before this commit they were part of the SystemMessage, and AgentInputFields still documents additional_instructions as a system tail. On OpenAI and Azure this lowers its instruction priority to user level, allowing later user content to override constraints and changing agent behavior merely by enabling caching. Preserve a system/developer role for the tail and make the request serializer target the stable instruction message or block for its breakpoint.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant