fix(claude): cap re-anchored cache_control at the 4-marker budget and keep single-object content turns

Anthropic accepts at most 4 blocks carrying cache_control per request. When the
client had already spent that budget, the re-anchor added a 5th marker and the
request was rejected with a non-retryable 400 that the failure path treated as
an account problem, retrying the same malformed body across the whole pool until
every account locked.

anchorClaudeCache now normalizes bare-object content, strips the invalid
cache_control carried by defer_loading tools, pins the 1h head anchors on the
last system block and last cacheable tool, then trims an over-budget body to 4
markers. The trim holds those head anchors and fills the remaining slots with the
tail-most message markers: a plain "keep the last four in document order" rule
drops the anchors first even though they lead document order, and skipping the
re-anchor at a spent budget left system/tools on the 5m default instead of 1h.

Some clients send content as a single block object rather than a one-element
array. Such a turn was dropped or zeroed on every leg that reads messages,
silently losing conversation history. normalizeMessageContent wraps it as a
one-block array on all four paths, and hasValidContent keeps it.
This commit is contained in:
Nick Nyanjui
2026-09-10 22:53:57 +07:00
committed by decolua
parent 122f23eebc
commit 8a81085a72
3 changed files with 330 additions and 5 deletions

View File

@@ -142,6 +142,13 @@ function systemReminderText(content) {
// Convert single Claude message - returns single message or array of messages
function convertClaudeMessage(msg) {
// Some clients send content as a single block object; normalize to the
// one-element array every branch below (the system-reminder fold included)
// expects. Must run BEFORE the role branch: systemReminderText only reads
// arrays and strings, so a bare-object system turn was dropped outright.
if (msg.content && typeof msg.content === "object" && !Array.isArray(msg.content)) {
msg.content = [msg.content];
}
// Mid-conversation system message -> user (per Anthropic placement rules)
if (msg.role === ROLE.SYSTEM) {
const text = systemReminderText(msg.content);