Skip to content

feat(openai-compat): opt-in cache_control breakpoints (OpenRouter Claude caching) - #7283

Closed
ngvoicu wants to merge 1 commit into
router-for-me:devfrom
ngvoicu:fix/openai-compat-cache-markers
Closed

ngvoicu wants to merge 1 commit into
router-for-me:devfrom
ngvoicu:fix/openai-compat-cache-markers

Conversation

@ngvoicu

@ngvoicu ngvoicu commented Nov 5, 2026

Copy link
Copy Markdown

Problem

OpenAI-compatible upstreams such as OpenRouter cache Claude only when the request marks what to cache (cache_control on content parts). CPA sends them the OpenAI chat format:

  • requests that arrive in Claude's format lose their markers in the Claude → OpenAI translation;
  • requests from Codex/OpenAI clients never had any.

So every request to Claude through such a provider re-bills the whole prompt. Measured through CPA 8.0.53: Claude via an OpenRouter entry never got a cache hit, while GPT models there (cached automatically) did.

Change

New opt-in per openai-compatibility provider, next to support-prompt-cache-key:

openai-compatibility:
  - name: OpenRouter
    base-url: https://openrouter.ai/api/v1
    support-cache-control: true

When it's set, chat-completions requests that carry no cache_control get Anthropic-style breakpoints:

  • Where: on the last system message and the latest user or tool message, mirroring CPA's own Claude breakpoints, which cover the whole conversation prefix. String content becomes one text part; otherwise the last non-empty text or image part is marked.
  • Lifetime: a Claude-format client asking for the 1-hour cache (ttl: "1h" on any block) keeps it.
  • Untouched: requests that already carry markers, Responses-format bodies, and providers without the option.

It's opt-in because strict OpenAI-compatible servers may reject unknown fields in content parts. OpenRouter documents cache_control for Anthropic models and ignores it elsewhere.

Verification

  • openai_compat_cache_control_test.go:

    • placement (system plus latest tool message, last non-empty part);
    • no change without opt-in, with existing markers, or for non-chat bodies;
    • 1h kept only for Claude sources;
    • executor end to end (Claude request → upstream body with two 1h breakpoints).
  • Live, real OpenRouter, anthropic/claude-haiku-5.5, two identical Claude-format requests (~12.6k tokens):

    support-cache-control request 1 request 2
    true cache_creation_input_tokens: 12620 cache_read_input_tokens: 12620
    false input_tokens: 12624 input_tokens: 12624 (no cache)
  • go build -o test-output ./cmd/server; gofmt clean on changed files; go test ./internal/runtime/executor/ ./internal/config/ ./internal/translator/... ./sdk/... green.

…hat need them

OpenAI-compatible upstreams such as OpenRouter cache Claude only when the
request marks what to cache, and the Claude-to-OpenAI translation drops the
client's markers, so requests through such a provider were never cached.

support-cache-control (per openai-compatibility provider, like
support-prompt-cache-key) adds Anthropic-style cache_control to the last
system message and the latest user/tool message of chat-completions
requests that carry none, mirroring CPA's own Claude breakpoints. A
Claude-format client's ttl "1h" is kept.

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c0c88ce9d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// SupportCacheControl adds Anthropic-style cache_control breakpoints to chat-completions requests that carry
// none, for upstreams that accept them (e.g. OpenRouter, which caches Claude only when asked). The client's
// requested lifetime (ttl "1h" in a Claude-format request) is kept.
SupportCacheControl bool `yaml:"support-cache-control,omitempty" json:"support-cache-control,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Propagate support-cache-control through Home credentials

In Home-enabled deployments, resolveCompatConfig rebuilds provider options from the selected auth's credential_options (openai_compat_executor.go:1154-1182), but that DTO and returned literal omit this new field and there is no support_cache_control attribute fallback. A Home credential that opts in therefore still reaches applyOpenAICompatCacheControlDoc with SupportCacheControl == false; propagate the flag through this resolver and the corresponding Home credential payload.

AGENTS.md reference: AGENTS.md:L63-L63

Useful? React with 👍 / 👎.

Comment on lines +78 to +82
part, err := ojson.SetBytes([]byte(`{"type":"text","text":""}`), "text", text)
if err != nil {
return
}
part, err = ojson.SetRawBytes(part, "cache_control", []byte(cacheControl))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mutate generated parts through ojson.Doc

When a selected system/user/tool message has string content, this branch builds its replacement by calling ojson.SetBytes and ojson.SetRawBytes on raw byte slices. That bypasses the required single-*ojson.Doc mutation pipeline and serializes an intermediate request fragment; build the part with an ojson.Doc or mutate the existing request Doc directly, then serialize once at the final exit.

AGENTS.md reference: AGENTS.md:L53-L53

Useful? React with 👍 / 👎.

Comment on lines +104 to +106
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{
OpenAICompatibility: []config.OpenAICompatibility{{Name: "router", SupportCacheControl: true}},
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Cover cache markers with the payload-barrier test

When support-cache-control is combined with a payload filter or override targeting the generated marker, this test fixture has no payload rules and therefore never proves that the finalizer remains authoritative; moving the injection after ApplyPayloadConfigWithRequestDoc would silently restore configured-away fields while the test stayed green. Extend this regression to remove or override a marker and assert the wire body for both Execute and ExecuteStream.

AGENTS.md reference: AGENTS.md:L54-L54

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

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-11-05T23:33:29.510593Z 4c0c88c 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.

@ngvoicu

ngvoicu commented Nov 5, 2026

Copy link
Copy Markdown
Author

Superseded by a broader OpenRouter change without a config flag: OpenAI-compatible entries pointing at OpenRouter get cache breakpoints, session affinity and model limits automatically (detected by base URL, like the existing reasoning-signature handling). New PR follows.

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