fix(claude): re-anchor passthrough cache breakpoints with 1h TTL

Passthrough kept the client's own cache_control markers, which point at
pre-normalization offsets. Once normalize/dedupe reshaped system and tools,
the breakpoints landed mid-array and the tail was re-cached every request.

- Pin the last system block and last tool at ttl 1h (was the client's 5m)
- Anchor the last assistant turn at 5m, falling back to the final message
  so a first turn still gets a breakpoint
- Fold mid-conversation system messages into the neighbouring user turn
  instead of hoisting them into body.system, where the volatile token
  counters invalidated the prefix on every request
- Run the anchoring after every token saver, at the final body

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
decolua
2026-08-14 16:08:30 +07:00
parent 345cdcf6a5
commit 7e5f5a8813
2 changed files with 97 additions and 20 deletions

View File

@@ -2,7 +2,7 @@ import { detectFormat, getTargetFormat, resolveTransport } from "../services/pro
import { translateRequest } from "../translator/index.js";
import { applyThinking, extractThinking, stripThinkingSuffix } from "../translator/concerns/thinkingUnified.js";
import { FORMATS } from "../translator/formats.js";
import { normalizeClaudePassthrough } from "../translator/formats/claude.js";
import { normalizeClaudePassthrough, anchorClaudeCache } from "../translator/formats/claude.js";
import { createStreamController } from "../utils/streamHandler.js";
import { refreshWithRetry } from "../services/tokenRefresh.js";
import { createRequestLogger } from "../utils/requestLogger.js";
@@ -275,6 +275,10 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
if (xf.length && log?.line) log.line(reqTag, "⚙", xf.join(" · "));
// Pin cache breakpoints to the final body — every saver above can reshape
// system/tools/messages, and a stale anchor costs a full prefix rewrite.
if (passthrough && clientTool === "claude") anchorClaudeCache(translatedBody);
const executor = getExecutor(provider);
trackPendingRequest(model, provider, connectionId, true);
appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => { });