Skip to content

fix(claude): hoist tool_result.content cache_control and enforce 4-block cap - #15647

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.52from
patrykkopycinski:fix/claude-cache-control-placement-400s
Oct 7, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.52from
patrykkopycinski:fix/claude-cache-control-placement-400s

Conversation

@patrykkopycinski

Copy link
Copy Markdown
Contributor

Summary

Claude rejects two request shapes that the OpenAI-to-Claude translator currently produces when a client puts cache_control on the content parts of role: "tool" messages:

messages.N.content.0.tool_result.content.0.cache_control: cache_control may not be specified within `tool_result.content`
A maximum of 4 blocks with cache_control may be provided. Found 5.

Observed in production on a self-hosted deployment: roughly 80% of claude-opus-5-5 requests from such a client returned 400 before this change.

prepareClaudeRequest now:

  • Hoists a cache_control marker found inside tool_result.content[] onto the enclosing tool_result block and strips the inner markers. If the block already has its own marker, that one is kept.
  • Caps the total number of cache_control blocks at 4 by keeping the last 4 (the most recent breakpoints, which give the best cache hits). The cap applies only to targets that support prompt caching. Other Claude-shape providers (e.g. kimi-coding) are left untouched.

String tool_result content is not modified.

Opus 5.5 model specs and thinking/effort handling are out of scope; #14546 covers them.

Related Issues

Validation

  • Change type: provider (translator)
  • Focused tests: new suite 7/7; 42 neighbouring suites that exercise claudeHelper / openai-to-claude 442/442
  • npm run lint, npm run typecheck:core, prettier --check: clean
  • Reconciled with release/v3.8.52 (branched from its tip, single commit)
  • Production-code change ships with a new automated test

Mutation check: with the hoist call removed, 3 of the 7 tests fail. With enforceCacheControlBlockLimit turned into a no-op, the cap test fails. Restoring either turns them green again.

Tests Added Or Updated

  • tests/unit/claude-cache-control-placement-400s.test.ts (new)

Coverage Notes

  • open-sse/translator/helpers/claudeHelper.ts: the three new helpers are covered by the new suite. It covers hoist in both preserve and non-preserve modes, an existing block-level marker, the cap above and at the limit, the non-caching provider exemption, and string content.

Reviewer Notes

  • When more than 4 markers are present, the cap drops the earliest ones. The alternative is to reject the request, which is what Anthropic already does, so dropping keeps the request usable at the cost of fewer cache breakpoints.

…ock cap

Claude 5.x rejects nested cache_control inside tool_result.content[] and
caps total cache_control blocks at 4; OmniRoute translations from OpenAI
role:tool messages triggered 400s on ~80% of opus-5-5 traffic in an
affected deployment.

- hoistToolResultCacheControl: move inner cache_control markers onto the
  tool_result block itself, strip inner duplicates
- enforceCacheControlBlockLimit: when more than 4 blocks carry
  cache_control, keep the last 4 (nearest the conversation head)
- both run in prepareClaudeRequest for every Claude-format target,
  independent of preserveCacheControl (shape requirements, not policy)

Verified: 7/7 new tests; hoist-disable mutation -> 3 red, cap no-op -> 1
red; 42 neighbouring suites referencing claudeHelper/openai-to-claude
442/442 green; lint, typecheck:core, prettier clean.
@patrykkopycinski
patrykkopycinski marked this pull request as ready for review October 6, 2026 15:12
@diegosouzapw
diegosouzapw merged commit 8ee4929 into cheaper-inference:release/v3.8.52 Oct 7, 2026
75 of 86 checks passed
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