Skip to content

fix(relayconvert): Claude Messages 转 OpenAI 时不再向非 OpenRouter 上游透传 cache_control - #6691

Open
suqi8 wants to merge 1 commit into
QuantumNous:mainfrom
suqi8:fix/claude-to-openai-cache-control-leak
Open

fix(relayconvert): Claude Messages 转 OpenAI 时不再向非 OpenRouter 上游透传 cache_control#6691
suqi8 wants to merge 1 commit into
QuantumNous:mainfrom
suqi8:fix/claude-to-openai-cache-control-leak

Conversation

@suqi8

@suqi8 suqi8 commented Aug 6, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

Claude Messages → OpenAI chat 的转换中,cache_control 这个 Anthropic/OpenRouter 专属字段被无条件透传给了所有上游。

同一个函数里,system 部分有 isOpenRouterClaude 判断保护(to_oai_chat_req.go:110),但消息内容部分漏了同样的判断(:156),导致普通 OpenAI 兼容上游也会收到这个它并不认识的字段。字段定义处的注释本身就标明了 // OpenRouter Params

这个字段造成两种不同的故障:

严格上游直接返回 400,即 #5982 报告的现象。该 issue 评论中 @haowang02 的对照测试已独立验证,400 的成因是 cache_control 本身,与 content 数组结构无关。

宽松上游不报错,但会静默破坏 prompt 缓存,这一面此前没有被记录。Claude Code 这类客户端会随对话轮次增长把缓存断点向后移动,所以这个标记每次落在不同的消息下标上。OpenAI 的自动缓存按最长公共前缀匹配,标记一移动,前缀匹配就在移动点被截断——本该轮轮命中的对话变成反复冷启动。

本 PR 把该判断提升为函数级变量 keepCacheControl,两处统一使用,对 OpenRouter 的行为保持不变。

同时补上了 prompt_cache_key:这条路径此前从未设置该字段(全仓库仅 oai_responses/to_oai_chat_req.go:91 的 Responses→Chat 路径传递过)。现从客户端的 metadata.user_id 转发,让上游能把同一会话路由到同一缓存节点。客户端未提供可用标识时保持不设置,而非自行生成——每次请求都变化的 key 比没有 key 更糟,会把本可命中的请求打散到不同节点。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 描述基于对代码逻辑的实际排查撰写,已人工审阅并经生产环境验证。
  • 非重复提交: 已搜索现有 Issues 与 PRs,未发现重复的修复 PR。
  • Bug fix 说明: 已关联 [Bug] Claude Messages → OpenAI Compatible 转换保留纯文本 content 数组及 cache_control,导致严格上游返回 400 #5982。这是明确的实现遗漏而非设计取舍——同一函数内两处等价逻辑,一处有保护一处没有,且字段定义处注释即标明用途限于 OpenRouter。
  • 变更理解: 已理解其工作原理及影响范围。
  • 范围聚焦: 仅改动目标文件与配套测试。
  • 本地验证: relaykit 子模块与主模块 ./relay/... 全量测试通过。
  • 安全合规: 无敏感凭据,符合项目代码规范。

📸 运行证明 / Proof of Work

生产环境实测,控制变量为同一渠道、同一模型 gpt-5.6-sol、输入规模相当:

入口路径 请求数 平均输入 token 缓存未命中率
/v1/responses(原生,不受影响) 118 60,985 14.4%
/v1/messages(转换,修复前) 55 62,994 74.5%
/v1/messages(转换,修复后) 79 13.3%

修复后转换路径与原生路径持平。以下干扰因素已排除:

  • 非缓存过期:间隔 30 秒内的连续请求同样有 51% 未命中
  • 非渠道切换:受影响会话全程固定同一渠道,且该渠道为单密钥
  • 非渠道配置:该渠道 param_override 为空,无 system_prompt 注入,强制缓存关闭

测试结果

ok  github.com/QuantumNous/new-api/relaykit/relayconvert           1.340s
ok  github.com/QuantumNous/new-api/relaykit/dto                    1.169s
ok  github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta  1.154s
ok  github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_chat       1.215s
ok  github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_responses  1.204s
ok  github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil   1.119s

主模块 ./relay/... 19 个包全部通过。

新增 4 组回归测试,覆盖 cache_control 门控的两个方向,以及 prompt_cache_key 的提取(含字段缺失、空值、非字符串、JSON 格式错误等边界)。已验证这些测试能真正捕获回归:临时还原旧逻辑后测试立即失败。

Summary by CodeRabbit

  • Bug Fixes

    • Improved prompt caching compatibility when converting Claude requests to OpenAI format.
    • Cache-control metadata is now retained for supported OpenRouter Claude models and excluded for other providers.
    • Valid user metadata can now be forwarded as a prompt cache key.
    • Invalid, empty, or missing metadata no longer produces an unusable cache key.
    • Requests without a cache key omit the field from serialized output.
  • Tests

    • Added coverage for provider-specific cache handling and metadata validation.

…eams

Claude Messages -> OpenAI chat conversion forwarded the Anthropic-only
`cache_control` marker to every upstream, not just OpenRouter. The system
branch was already guarded, but the message-content branch was not, so a
plain OpenAI-compatible upstream received a field it does not understand.

Strict upstreams reject it outright with a 400, which is what QuantumNous#5982
reports. Permissive upstreams accept it and fail silently in a worse way:
it destroys prompt caching. Clients such as Claude Code move the cache
breakpoint forward as a conversation grows, so the marker lands on a
different message index on each request. OpenAI's automatic caching keys
on the longest common prefix, and the moved marker truncates that match at
exactly the point it moved -- so a conversation that should hit cache on
every turn keeps cold-starting instead.

Also forward the client's `metadata.user_id` as `prompt_cache_key`, which
this path never set. A stable cache key lets the upstream route a
conversation to the same cache node. When the client sends no usable
identifier the field is left unset rather than synthesised: a key that
varies per request is worse than no key at all.

Measured on a production deployment, comparing the same channel and model
at the same input size, before and after this change:

  /v1/messages -> GPT   cache-miss rate  74.5%  ->  13.3%
  /v1/responses (native, unaffected)     14.4%

The converted path now matches the native path, which is what it should
have done all along. Regression tests cover both directions of the
cache_control gate and the prompt_cache_key extraction, including
malformed and non-string metadata.

Introduced in QuantumNous#983. Refs QuantumNous#5982, QuantumNous#6538.
Copilot AI lite review requested due to automatic review settings August 6, 2026 12:11
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The Claude-to-OpenAI converter now restricts cache_control forwarding to OpenRouter Anthropic requests. It also derives prompt_cache_key from valid trimmed metadata.user_id values. Tests cover provider filtering, metadata validation, and JSON omission.

Changes

Claude cache conversion

Layer / File(s) Summary
Cache-control routing
relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go, relaykit/relayconvert/claude_to_openai_cache_test.go
The converter preserves cache_control for OpenRouter Anthropic requests and strips it for other upstreams. Tests cover system and text content.
Prompt cache-key propagation
relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go, relaykit/relayconvert/claude_to_openai_cache_test.go
The converter copies valid trimmed metadata.user_id values to PromptCacheKey. Invalid or empty values remain unset and are omitted from serialized JSON.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • QuantumNous/new-api issue 6538 — Covers related prompt_cache_key propagation across Claude-to-OpenAI conversion paths.

Possibly related PRs

Suggested reviewers: copilot

Poem

A rabbit checks the cache with care,
Keeps OpenRouter markers there.
Invalid keys fade out of sight,
Trimmed user IDs travel right.
Clean JSON hops through the night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing cache_control from reaching non-OpenRouter upstreams during Claude-to-OpenAI conversion.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI 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.

Pull request overview

This PR fixes Claude Messages → OpenAI Chat request conversion so Anthropic/OpenRouter-specific cache_control is no longer forwarded to non-OpenRouter upstreams, preventing strict upstream 400s and avoiding prompt-cache disruption. It also adds prompt_cache_key propagation from metadata.user_id to restore cache affinity for converted requests.

Changes:

  • Introduce a single keepCacheControl gate and apply it consistently to both system and message content conversion paths.
  • Populate prompt_cache_key from Claude request metadata.user_id when present and usable.
  • Add regression tests covering cache_control gating and prompt_cache_key extraction/omission behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go Gates cache_control forwarding to OpenRouter+Anthropic models only, and forwards metadata.user_id into prompt_cache_key when available.
relaykit/relayconvert/claude_to_openai_cache_test.go Adds tests ensuring cache_control is stripped/kept correctly and prompt_cache_key is extracted/omitted as intended.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
relaykit/relayconvert/claude_to_openai_cache_test.go (1)

59-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expand the cache-control contract matrix.

The tests do not cover an OpenRouter request for a non-Anthropic model. They also do not cover structured system content.

Add regression cases where OpenRouterDialect is true and UpstreamModelName is openai/gpt-4o, and where a system block contains cache_control. Both cases must omit cache_control. This protects both terms of keepCacheControl and the changed system branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relaykit/relayconvert/claude_to_openai_cache_test.go` around lines 59 - 74,
Expand the cache-control tests around ClaudeMessagesRequestToOpenAIChat with an
OpenRouter non-Anthropic model (UpstreamModelName “openai/gpt-4o”) and a
structured system-content block containing cache_control. Assert cache_control
is omitted in both cases, while preserving the existing OpenRouter Anthropic
test that expects it to be forwarded.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@relaykit/relayconvert/claude_to_openai_cache_test.go`:
- Around line 59-74: Expand the cache-control tests around
ClaudeMessagesRequestToOpenAIChat with an OpenRouter non-Anthropic model
(UpstreamModelName “openai/gpt-4o”) and a structured system-content block
containing cache_control. Assert cache_control is omitted in both cases, while
preserving the existing OpenRouter Anthropic test that expects it to be
forwarded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 449d3095-5f62-4a64-9242-9ef30c65dd40

📥 Commits

Reviewing files that changed from the base of the PR and between 0cd9dc8 and 5a0aef2.

📒 Files selected for processing (2)
  • relaykit/relayconvert/claude_to_openai_cache_test.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go

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.

2 participants