Files
CLIProxyAPI/internal/runtime/executor/claude_executor_cloaking.go
sususu aec70dfec4 fix(claude): gate CCH signing by upstream origin and validate fingerprint-profile
Claude Code 2.1.220 through 2.1.234 emit the cch attribution only for
firstParty on api.anthropic.com and for vertex; every other backend sends
the billing header unsigned. CPA had dropped its endpoint check, so an
opted-in API key signed a per-request hash on any gateway and could bust
that gateway's prompt cache.

- Restore the endpoint gate in claudeCCHSigningEnabled: a real Claude OAuth
  credential still signs on every upstream, because a downstream Claude Code
  pointed at CPA cannot produce that value itself, while a claude-code-cli
  API key signs only on api.anthropic.com or Vertex
- Drop the unused origin parameter from Claude fingerprint policy resolution
  and restore the original resolveClaudeWirePolicy signature; the wire profile
  follows the credential and only CCH follows the origin
- Add config.NormalizeClaudeFingerprintProfile / ValidateClaudeFingerprintProfile
  as the single source of truth for fingerprint-profile values
- Reject unknown fingerprint-profile values in the Management API, and warn
  once per distinct value at request time instead of on every resolution,
  which previously logged about four warnings per request for one typo
- Preserve unrecognized values through config sanitization so rewriting a
  config file never discards operator input
- Update config.example.yaml and tests for the origin-scoped CCH behavior
2026-08-18 18:06:40 +08:00

1753 lines
59 KiB
Go

package executor
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/gin-gonic/gin"
)
func resolveIncomingClaudeHeaders(ctx context.Context, incoming http.Header) http.Header {
resolved := make(http.Header)
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
resolved = ginCtx.Request.Header.Clone()
}
for key, values := range incoming {
resolved[key] = append([]string(nil), values...)
}
return resolved
}
func detectIncomingClaudeCodeRequest(ctx context.Context, incoming http.Header, payload []byte, countTokens bool, cfg *config.Config) (http.Header, helps.ClaudeCodeRequestDetection) {
resolved := resolveIncomingClaudeHeaders(ctx, incoming)
return resolved, helps.DetectClaudeCodeRequest(resolved, payload, countTokens, cfg)
}
// getWorkloadFromContext extracts workload identifier from the gin request headers.
func getWorkloadFromContext(ctx context.Context) string {
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
return strings.TrimSpace(ginCtx.GetHeader("X-CPA-Claude-Workload"))
}
return ""
}
// getCloakConfigFromAuth extracts cloak configuration from the auth's attributes,
// falling back to its stored metadata (the raw OAuth/token JSON). Returns
// (cloakMode, strictMode, sensitiveWords, cacheUserID); an empty cloakMode means
// the credential did not explicitly configure a mode.
func getCloakConfigFromAuth(auth *cliproxyauth.Auth) (cloakMode string, strictMode bool, sensitiveWords []string, cacheUserID bool) {
if auth == nil {
return "", false, nil, false
}
// lookupCloakAttr prefers the executor-facing Attributes, then falls back to the
// raw metadata blob (e.g. the OAuth/token JSON) so file-based credentials can
// carry cloak settings without a matching claude-api-key config entry.
lookupCloakAttr := func(key string) string {
if auth.Attributes != nil {
if value := strings.TrimSpace(auth.Attributes[key]); value != "" {
return value
}
}
if value := claudeauth.ReadMetadataString(&auth.Metadata, key); value != "" {
return strings.TrimSpace(value)
}
return ""
}
// An empty cloakMode means this credential did not explicitly configure a mode,
// allowing the caller to fall back to the global/default behavior.
cloakMode = lookupCloakAttr("cloak_mode")
strictMode = strings.EqualFold(lookupCloakAttr("cloak_strict_mode"), "true")
if wordsStr := lookupCloakAttr("cloak_sensitive_words"); wordsStr != "" {
sensitiveWords = strings.Split(wordsStr, ",")
for i := range sensitiveWords {
sensitiveWords[i] = strings.TrimSpace(sensitiveWords[i])
}
}
cacheUserID = strings.EqualFold(lookupCloakAttr("cloak_cache_user_id"), "true")
return cloakMode, strictMode, sensitiveWords, cacheUserID
}
// injectFakeUserID generates and injects a fake user ID into the request metadata.
// When useCache is false, a new user ID is generated for every call.
func injectFakeUserID(ctx context.Context, payload []byte, apiKey string, useCache bool) ([]byte, error) {
generateID := func() (string, error) {
if useCache {
return helps.CachedUserIDRequired(ctx, apiKey)
}
sessionID, errSessionID := helps.CachedSessionIDRequired(ctx, apiKey)
if errSessionID != nil {
return "", errSessionID
}
return helps.GenerateFakeUserIDWithSessionID(sessionID), nil
}
metadata := gjson.GetBytes(payload, "metadata")
if !metadata.Exists() {
userID, errUserID := generateID()
if errUserID != nil {
return nil, errUserID
}
payload, _ = sjson.SetBytes(payload, "metadata.user_id", userID)
return payload, nil
}
existingUserID := gjson.GetBytes(payload, "metadata.user_id").String()
if existingUserID == "" || !helps.IsValidUserID(existingUserID) {
userID, errUserID := generateID()
if errUserID != nil {
return nil, errUserID
}
payload, _ = sjson.SetBytes(payload, "metadata.user_id", userID)
}
return payload, nil
}
// fingerprintSalt is the salt used by Claude Code to compute the 3-char build fingerprint.
const fingerprintSalt = "59cf53e54c78"
// computeFingerprint computes the 3-char build fingerprint that Claude Code embeds in cc_version.
// Algorithm: SHA256(salt + messageText[4] + messageText[7] + messageText[20] + version)[:3]
func computeFingerprint(messageText, version string) string {
indices := [3]int{4, 7, 20}
runes := []rune(messageText)
var sb strings.Builder
for _, idx := range indices {
if idx < len(runes) {
sb.WriteRune(runes[idx])
} else {
sb.WriteRune('0')
}
}
input := fingerprintSalt + sb.String() + version
h := sha256.Sum256([]byte(input))
return hex.EncodeToString(h[:])[:3]
}
// generateBillingHeader creates the x-anthropic-billing-header text block that
// Claude Code prepends to its system prompt. cch is present only on signed paths.
func generateBillingHeader(cchSigning bool, version, messageText, entrypoint, workload string) string {
if entrypoint == "" {
entrypoint = "cli"
}
buildHash := computeFingerprint(messageText, version)
workloadPart := ""
if workload != "" {
workloadPart = fmt.Sprintf(" cc_workload=%s;", workload)
}
if cchSigning {
return fmt.Sprintf("x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=%s; cch=00000;%s", version, buildHash, entrypoint, workloadPart)
}
return fmt.Sprintf("x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=%s;%s", version, buildHash, entrypoint, workloadPart)
}
func claudeBillingFingerprintMessageText(payload []byte) string {
messageText := ""
gjson.GetBytes(payload, "messages").ForEach(func(_, message gjson.Result) bool {
if message.Get("role").String() != "user" {
return true
}
content := message.Get("content")
candidate := ""
if content.Type == gjson.String {
candidate = content.String()
} else if content.IsArray() {
content.ForEach(func(_, part gjson.Result) bool {
if part.Get("type").String() == "text" {
candidate = part.Get("text").String()
}
return true
})
}
if candidate != "" {
messageText = candidate
}
return true
})
return messageText
}
func claudeCCHFallbackBillingHeader(ctx context.Context, cfg *config.Config, payload []byte, entrypoint string) string {
return generateBillingHeader(
true,
helps.DefaultClaudeVersion(cfg),
claudeBillingFingerprintMessageText(payload),
entrypoint,
getWorkloadFromContext(ctx),
)
}
const claudeCodeCLIIdentity = "You are Claude Code, Anthropic's official CLI for Claude."
func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte {
return checkSystemInstructionsWithSigningMode(payload, strictMode, false, "2.1.220", "cli", "")
}
// checkSystemInstructionsWithSigningMode keeps the top-level system in Claude
// Code's minimal CLI shape. Each caller system block is preserved as a separate
// mid-conversation system message after the first user turn, where supported
// Claude models give it operator-level authority without changing the cached
// top-level prefix.
func checkSystemInstructionsWithSigningMode(payload []byte, strictMode bool, cchSigning bool, version, entrypoint, workload string) []byte {
return checkSystemInstructionsWithSigningModeAt(payload, strictMode, cchSigning, version, entrypoint, workload, time.Now())
}
func checkSystemInstructionsWithSigningModeAt(payload []byte, strictMode bool, cchSigning bool, version, entrypoint, workload string, now time.Time) []byte {
system := gjson.GetBytes(payload, "system")
messageText := claudeBillingFingerprintMessageText(payload)
billingText := generateBillingHeader(cchSigning, version, messageText, entrypoint, workload)
billingBlock := buildTextBlock(billingText, nil)
agentBlock := buildTextBlock(claudeCodeCLIIdentity, &claudeCodeCacheControl)
payload, _ = sjson.SetRawBytes(payload, "system", []byte("["+billingBlock+","+agentBlock+"]"))
if strictMode {
return injectClaudeCodeCurrentDate(payload, now)
}
forwardedSystemBlocks := collectForwardedClaudeSystemPromptBlocks(system)
if len(forwardedSystemBlocks) == 0 {
return injectClaudeCodeCurrentDate(payload, now)
}
if claudeUsesLegacySystemReminder(payload) {
payload = prependClaudeSystemRemindersToFirstUserMessage(payload, forwardedSystemBlocks)
} else {
// Unknown and future model IDs optimistically use the authoritative
// mid-conversation system role. Only empirically unsupported legacy IDs
// stay on the user-reminder compatibility path.
payload = insertClaudeMidConversationSystemMessages(payload, forwardedSystemBlocks)
}
return injectClaudeCodeCurrentDate(payload, now)
}
// relocateClaudeSystemPromptForCountTokens keeps a cloaked count_tokens request
// in Claude Code's measured shape, which carries only model, messages and tools.
// The Claude Code system blocks are therefore not installed here, but each caller
// system block still has to be accounted for, so it is relocated into messages
// using the same positional mapping as the Messages path. That keeps the counted
// tokens aligned with the request the caller is about to send while preventing a
// third-party system prompt from reaching Anthropic in the system slot.
func relocateClaudeSystemPromptForCountTokens(payload []byte, strictMode bool) []byte {
system := gjson.GetBytes(payload, "system")
if !system.Exists() {
return payload
}
// Strict mode drops caller prompts on the Messages path, so it must not
// reintroduce them here either.
var forwardedSystemBlocks []string
if !strictMode {
forwardedSystemBlocks = collectForwardedClaudeSystemPromptBlocks(system)
}
updated, errDelete := sjson.DeleteBytes(payload, "system")
if errDelete != nil {
return payload
}
payload = updated
if len(forwardedSystemBlocks) == 0 {
return payload
}
if claudeUsesLegacySystemReminder(payload) {
return prependClaudeSystemRemindersToFirstUserMessage(payload, forwardedSystemBlocks)
}
return insertClaudeMidConversationSystemMessages(payload, forwardedSystemBlocks)
}
// claudeLegacySystemReminderModels lists the official Anthropic model IDs and
// aliases that reject a mid-conversation role=system message. Entries mirror the
// "claude" provider in internal/registry/models/models.json plus Anthropic's own
// bare and "-latest" aliases. Other providers' synthetic IDs do not belong here.
var claudeLegacySystemReminderModels = map[string]struct{}{
"claude-3-5-haiku-20241022": {},
"claude-3-5-haiku-latest": {},
"claude-3-7-sonnet-20250219": {},
"claude-3-7-sonnet-latest": {},
"claude-haiku-4-5": {},
"claude-haiku-4-5-20251001": {},
"claude-opus-4": {},
"claude-opus-4-20250514": {},
"claude-opus-4-1": {},
"claude-opus-4-1-20250805": {},
"claude-opus-4-5": {},
"claude-opus-4-5-20251101": {},
"claude-opus-4-6": {},
"claude-opus-4-7": {},
"claude-sonnet-4": {},
"claude-sonnet-4-20250514": {},
"claude-sonnet-4-5": {},
"claude-sonnet-4-5-20250929": {},
"claude-sonnet-4-6": {},
}
func claudeUsesLegacySystemReminder(payload []byte) bool {
model := strings.ToLower(strings.TrimSpace(gjson.GetBytes(payload, "model").String()))
if slash := strings.LastIndexByte(model, '/'); slash >= 0 {
model = model[slash+1:]
}
_, legacy := claudeLegacySystemReminderModels[model]
return legacy
}
// claudeCallerSystemBlockError reports a caller system block that Claude cannot
// carry in any system slot. It is request-scoped: no other credential or upstream
// model can accept the same body, so the request must not be retried.
type claudeCallerSystemBlockError struct {
statusErr
}
func (claudeCallerSystemBlockError) IsRequestScoped() bool {
return true
}
func newClaudeCallerSystemBlockError(index int, blockType string) error {
if blockType == "" {
blockType = "unknown"
}
return claudeCallerSystemBlockError{statusErr{
code: http.StatusBadRequest,
msg: fmt.Sprintf("invalid_request_error: system.%d.type: Input should be 'text'. "+
"System instructions support text only, but this block has type %q. "+
"Move non-text content into a user message.", index, blockType),
}}
}
// claudeMidSystemMessageModelError reports a mid-conversation
// {"role":"system"} turn addressed to a first-party model that cannot carry
// it. It is request-scoped for the same reason as claudeCallerSystemBlockError:
// the body is incompatible with the model rather than evidence of unhealthy
// credentials, so no credential should be cooled or retried.
type claudeMidSystemMessageModelError struct {
statusErr
}
func (claudeMidSystemMessageModelError) IsRequestScoped() bool {
return true
}
// The turn is not always the caller's. CPA normally reconciles a cloaked turn
// when a payload rule changes the model to legacy, but it deliberately gives up
// if the rule also rewrites the tracked messages and their provenance is no
// longer exact. The wording therefore states the model's requirement instead of
// assuming the caller created the turn.
func newClaudeMidSystemMessageModelError(model string) error {
if model == "" {
model = "unknown"
}
return claudeMidSystemMessageModelError{statusErr{
code: http.StatusBadRequest,
msg: fmt.Sprintf("invalid_request_error: role 'system' is not supported on this model. "+
"Model %q predates mid-conversation system turns, so system instructions must "+
"stay in the top-level system field for it.", model),
}}
}
// validateClaudeMidSystemMessageModel rejects a request that pairs a legacy
// model with a caller's mid-conversation {"role":"system"} turn.
//
// Anthropic answers that pairing with a guaranteed rejection, verified on both
// /v1/messages and /v1/messages/count_tokens:
//
// 400 role 'system' is not supported on this model
//
// The native client never produces it either: it gates the turn on the model,
// which is also why claudeCodeCLIBetas withholds
// mid-conversation-system-2026-04-07 for these IDs. In 314 captured native
// requests the turn appears only on claude-opus-5 and claude-sonnet-5, and on
// none of the 43 requests addressed to a model in
// claudeLegacySystemReminderModels.
//
// Three conditions keep the check inside the evidence that produced it:
//
// - firstPartyAnthropic, because the rejection was measured against
// api.anthropic.com. A third-party gateway may map these model IDs onto
// something that accepts the turn, and answering locally would also stop
// failover to another credential or base URL.
// - confirmedClaudeCode, because a client that still matches the native
// fingerprint owns its wire. It gates the turn itself, so its body is
// forwarded untouched and any upstream error reaches it unchanged.
// - the pairing itself, so unknown and future model IDs stay optimistic in
// the same way checkSystemInstructions treats them.
//
// Operators who prefer the turn folded into the system slot can still set
// rebuild_mid_system_message, which runs before this check.
//
// The error is request-scoped: the body/model pairing is invalid independently
// of first-party credential health, so no credential should be cooled or
// retried.
func validateClaudeMidSystemMessageModel(payload []byte, confirmedClaudeCode, firstPartyAnthropic bool) error {
if confirmedClaudeCode || !firstPartyAnthropic {
return nil
}
if !claudeUsesLegacySystemReminder(payload) || !claudePayloadHasMidSystemMessage(payload) {
return nil
}
return newClaudeMidSystemMessageModelError(gjson.GetBytes(payload, "model").String())
}
// validateClaudeCallerSystemBlocks rejects caller system content that cannot keep
// its operator authority. Verified against api.anthropic.com on 2026-08-03: the
// top-level system field answers "system.<i>.type: Input should be 'text'" for
// image, document and unknown block types, and a role=system message answers
// "role 'system' supports text, tool_addition, and tool_removal blocks only".
// Cloaking relocates caller blocks into one of those two slots, so a non-text
// block has no destination. Failing here keeps the caller's instructions from
// being silently dropped, and costs no upstream attempt.
func validateClaudeCallerSystemBlocks(system gjson.Result) error {
if !system.IsArray() {
// A string system prompt is text by definition.
return nil
}
var blockErr error
index := 0
system.ForEach(func(_, part gjson.Result) bool {
if strings.TrimSpace(part.Get("type").String()) != "text" {
blockErr = newClaudeCallerSystemBlockError(index, strings.TrimSpace(part.Get("type").String()))
return false
}
index++
return true
})
return blockErr
}
func collectForwardedClaudeSystemPromptBlocks(system gjson.Result) []string {
var blocks []string
appendText := func(text string) {
if strings.TrimSpace(text) == "" || util.IsClaudeCodeAttributionSystemText(text) || text == claudeCodeCLIIdentity {
return
}
blocks = append(blocks, text)
}
if system.IsArray() {
system.ForEach(func(_, part gjson.Result) bool {
if part.Get("type").String() == "text" {
appendText(part.Get("text").String())
}
return true
})
} else if system.Type == gjson.String {
appendText(system.String())
}
return blocks
}
// buildTextBlock constructs a JSON text block with JSON.stringify-compatible
// HTML characters. encoding/json's default \u003c escaping would change the
// exact currentDate bytes and therefore the final CCH.
func buildTextBlock(text string, cacheControl *claudeCacheControl) string {
block := `{"type":"text","text":` + marshalJSONStringWithoutHTMLEscape(text)
if cacheControl != nil && cacheControl.Type != "" {
block += `,"cache_control":{"type":` + marshalJSONStringWithoutHTMLEscape(cacheControl.Type)
if cacheControl.TTL != "" {
block += `,"ttl":` + marshalJSONStringWithoutHTMLEscape(cacheControl.TTL)
}
block += "}"
}
return block + "}"
}
func marshalJSONStringWithoutHTMLEscape(value string) string {
var encoded bytes.Buffer
encoder := json.NewEncoder(&encoded)
encoder.SetEscapeHTML(false)
_ = encoder.Encode(value)
return strings.TrimSuffix(encoded.String(), "\n")
}
func prependClaudeSystemRemindersToFirstUserMessage(payload []byte, texts []string) []byte {
firstUserIdx := firstClaudeUserMessageIndex(payload)
if firstUserIdx < 0 || len(texts) == 0 {
return payload
}
reminderTexts := make([]string, 0, len(texts))
for _, text := range texts {
reminderTexts = append(reminderTexts, claudeCallerSystemReminder(text))
}
contentPath := fmt.Sprintf("messages.%d.content", firstUserIdx)
content := gjson.GetBytes(payload, contentPath)
if content.IsArray() {
blocks := content.Array()
existing := make(map[string]int, len(blocks))
for _, block := range blocks {
if block.Get("type").String() == "text" {
existing[block.Get("text").String()]++
}
}
reminderBlocks := make([]string, 0, len(reminderTexts))
for _, reminderText := range reminderTexts {
if existing[reminderText] > 0 {
existing[reminderText]--
continue
}
reminderBlocks = append(reminderBlocks, buildTextBlock(reminderText, nil))
}
if len(reminderBlocks) == 0 {
return payload
}
insertAt := 0
for insertAt < len(blocks) && blocks[insertAt].Get("type").String() == "tool_result" {
insertAt++
}
rawBlocks := make([]string, 0, len(blocks)+len(reminderBlocks))
for idx, block := range blocks {
if idx == insertAt {
rawBlocks = append(rawBlocks, reminderBlocks...)
}
rawBlocks = append(rawBlocks, block.Raw)
}
if insertAt == len(blocks) {
rawBlocks = append(rawBlocks, reminderBlocks...)
}
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte("["+strings.Join(rawBlocks, ",")+"]"))
} else if content.Type == gjson.String {
rawBlocks := make([]string, 0, len(reminderTexts)+1)
for _, reminderText := range reminderTexts {
rawBlocks = append(rawBlocks, buildTextBlock(reminderText, nil))
}
rawBlocks = append(rawBlocks, buildTextBlock(content.String(), nil))
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte("["+strings.Join(rawBlocks, ",")+"]"))
}
return payload
}
func claudeCallerSystemReminder(text string) string {
var reminder strings.Builder
reminder.WriteString("<system-reminder>\n")
reminder.WriteString(text)
if !strings.HasSuffix(text, "\n") {
reminder.WriteByte('\n')
}
reminder.WriteString("</system-reminder>")
return reminder.String()
}
func insertClaudeMidConversationSystemMessages(payload []byte, texts []string) []byte {
firstUserIdx := firstClaudeUserMessageIndex(payload)
if firstUserIdx < 0 || len(texts) == 0 {
return payload
}
messages := gjson.GetBytes(payload, "messages")
if !messages.IsArray() {
return payload
}
messageBlocks := messages.Array()
insertAt := firstUserIdx + 1
for insertAt < len(messageBlocks) && messageBlocks[insertAt].Get("role").String() == "user" {
insertAt++
}
if len(messageBlocks)-insertAt >= len(texts) {
matches := true
for idx, text := range texts {
message := messageBlocks[insertAt+idx]
if message.Get("role").String() != "system" || claudeMessageContentText(message.Get("content")) != text {
matches = false
break
}
}
if matches {
return payload
}
}
systemMessages := make([]string, 0, len(texts))
for _, text := range texts {
content := "[" + buildTextBlock(text, &claudeCodeCacheControl) + "]"
systemMessages = append(systemMessages, `{"role":"system","content":`+content+"}")
}
rawMessages := make([]string, 0, len(messageBlocks)+len(systemMessages))
for idx, message := range messageBlocks {
if idx == insertAt {
rawMessages = append(rawMessages, systemMessages...)
}
rawMessages = append(rawMessages, message.Raw)
}
if insertAt == len(messageBlocks) {
rawMessages = append(rawMessages, systemMessages...)
}
payload, _ = sjson.SetRawBytes(payload, "messages", []byte("["+strings.Join(rawMessages, ",")+"]"))
return payload
}
func claudeMessageContentText(content gjson.Result) string {
if content.Type == gjson.String {
return content.String()
}
if !content.IsArray() {
return ""
}
var parts []string
content.ForEach(func(_, block gjson.Result) bool {
if block.Get("type").String() == "text" {
parts = append(parts, block.Get("text").String())
}
return true
})
return strings.Join(parts, "\n\n")
}
// claudeCodeSystemPlacementState identifies only the role=system turns that CPA
// itself inserted while cloaking. Caller-owned turns are deliberately excluded:
// if one is paired with a legacy model, validateClaudeMidSystemMessageModel must
// still return 400 instead of silently rewriting the caller's wire.
type claudeCodeSystemPlacementState struct {
insertAt int
insertedRaw []string
texts []string
}
// captureClaudeCodeSystemPlacement records CPA's modern-model system placement
// immediately after cloaking. The message-count increase is part of the proof:
// insertClaudeMidConversationSystemMessages returns without inserting when the
// same turns already exist, and those pre-existing turns belong to the caller.
func captureClaudeCodeSystemPlacement(before, after []byte, cloaked bool) claudeCodeSystemPlacementState {
if !cloaked || claudeUsesLegacySystemReminder(before) {
return claudeCodeSystemPlacementState{}
}
texts := collectForwardedClaudeSystemPromptBlocks(gjson.GetBytes(before, "system"))
if len(texts) == 0 {
return claudeCodeSystemPlacementState{}
}
beforeMessages := gjson.GetBytes(before, "messages").Array()
afterMessages := gjson.GetBytes(after, "messages").Array()
if len(afterMessages) != len(beforeMessages)+len(texts) {
return claudeCodeSystemPlacementState{}
}
firstUserIdx := firstClaudeUserMessageIndex(before)
if firstUserIdx < 0 {
return claudeCodeSystemPlacementState{}
}
insertAt := firstUserIdx + 1
for insertAt < len(beforeMessages) && beforeMessages[insertAt].Get("role").String() == "user" {
insertAt++
}
if insertAt+len(texts) > len(afterMessages) {
return claudeCodeSystemPlacementState{}
}
insertedRaw := make([]string, len(texts))
for idx, text := range texts {
message := afterMessages[insertAt+idx]
if message.Get("role").String() != "system" || claudeMessageContentText(message.Get("content")) != text {
return claudeCodeSystemPlacementState{}
}
insertedRaw[idx] = message.Raw
}
return claudeCodeSystemPlacementState{
insertAt: insertAt,
insertedRaw: insertedRaw,
texts: append([]string(nil), texts...),
}
}
// reconcileClaudeCodeSystemPlacementAfterPayload repairs an otherwise stale
// placement decision when payload rules change the final model from modern to
// legacy. It removes only the exact contiguous turns captured above and replays
// their text through the existing legacy <system-reminder> path. If any payload
// rule also changed those messages, reconciliation fails closed and leaves the
// final validation guard to return 400.
func reconcileClaudeCodeSystemPlacementAfterPayload(payload []byte, state claudeCodeSystemPlacementState) []byte {
if len(state.insertedRaw) == 0 || !claudeUsesLegacySystemReminder(payload) {
return payload
}
messages := gjson.GetBytes(payload, "messages").Array()
if state.insertAt < 0 || state.insertAt+len(state.insertedRaw) > len(messages) {
return payload
}
for idx, raw := range state.insertedRaw {
if messages[state.insertAt+idx].Raw != raw {
return payload
}
}
rawMessages := make([]string, 0, len(messages)-len(state.insertedRaw))
for idx, message := range messages {
if idx >= state.insertAt && idx < state.insertAt+len(state.insertedRaw) {
continue
}
rawMessages = append(rawMessages, message.Raw)
}
updated, errSet := sjson.SetRawBytes(payload, "messages", []byte("["+strings.Join(rawMessages, ",")+"]"))
if errSet != nil {
return payload
}
return prependClaudeSystemRemindersToFirstUserMessage(updated, state.texts)
}
// claudeCodeLocalDate reproduces Claude Code 2.1.220's wcs() helper:
// new Date(), local calendar fields, and zero-padded YYYY-MM-DD components.
func claudeCodeLocalDate(now time.Time) string {
year, month, day := now.Date()
return fmt.Sprintf("%04d-%02d-%02d", year, int(month), day)
}
func claudeCodeCurrentTime(cfg *config.Config, auth *cliproxyauth.Auth) time.Time {
return time.Now().In(claudeCodeTimezone(cfg, auth))
}
func claudeCodeTimezone(cfg *config.Config, auth *cliproxyauth.Auth) *time.Location {
if timezone := claudeCredentialTimezone(auth); timezone != "" {
if location, errLocation := time.LoadLocation(timezone); errLocation == nil {
return location
}
}
if cfg == nil {
return time.Local
}
timezone := strings.TrimSpace(cfg.ClaudeHeaderDefaults.Timezone)
if timezone == "" {
return time.Local
}
location, errLocation := time.LoadLocation(timezone)
if errLocation != nil {
return time.Local
}
return location
}
func claudeCredentialTimezone(auth *cliproxyauth.Auth) string {
if auth == nil {
return ""
}
if auth.Attributes != nil {
if timezone := strings.TrimSpace(auth.Attributes["timezone"]); timezone != "" {
return timezone
}
}
return strings.TrimSpace(claudeauth.ReadMetadataString(&auth.Metadata, "timezone"))
}
func claudeCodeCurrentDateReminder(now time.Time) string {
return fmt.Sprintf(`<system-reminder>
As you answer the user's questions, you can use the following context:
# currentDate
Today's date is %s.
IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
</system-reminder>
`, claudeCodeLocalDate(now))
}
func firstClaudeUserMessageIndex(payload []byte) int {
messages := gjson.GetBytes(payload, "messages")
if !messages.Exists() || !messages.IsArray() {
return -1
}
firstUserIdx := -1
messages.ForEach(func(idx, msg gjson.Result) bool {
if msg.Get("role").String() == "user" {
firstUserIdx = int(idx.Int())
return false
}
return true
})
return firstUserIdx
}
func isClaudeCodeContextReminder(text string) bool {
return strings.HasPrefix(text, "<system-reminder>") && strings.Contains(text, "</system-reminder>")
}
func isClaudeCodeCurrentDateReminder(text string) bool {
return strings.HasPrefix(text, "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# currentDate\nToday's date is ")
}
func injectClaudeCodeCurrentDate(payload []byte, now time.Time) []byte {
firstUserIdx := firstClaudeUserMessageIndex(payload)
if firstUserIdx < 0 {
return payload
}
contentPath := fmt.Sprintf("messages.%d.content", firstUserIdx)
content := gjson.GetBytes(payload, contentPath)
dateText := claudeCodeCurrentDateReminder(now)
dateBlock := buildTextBlock(dateText, nil)
if content.Type == gjson.String {
userBlock := buildTextBlock(content.String(), &claudeCodeCacheControl)
newArray := "[" + dateBlock + "," + userBlock + "]"
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte(newArray))
return payload
}
if !content.IsArray() {
return payload
}
blocks := content.Array()
rawBlocks := make([]string, 0, len(blocks)+1)
actualTextCached := false
for _, block := range blocks {
if block.Get("type").String() == "text" {
text := block.Get("text").String()
if isClaudeCodeCurrentDateReminder(text) {
continue
}
if !actualTextCached && !isClaudeCodeContextReminder(text) {
rawBlocks = append(rawBlocks, withEphemeralCacheControl(block.Raw))
actualTextCached = true
continue
}
}
rawBlocks = append(rawBlocks, block.Raw)
}
// Anthropic requires the user message following an assistant tool_use turn
// to lead with its tool_result blocks, so the reminder goes after them.
// Every other content shape keeps the native first-block placement.
insertAt := 0
for insertAt < len(rawBlocks) && gjson.Parse(rawBlocks[insertAt]).Get("type").String() == "tool_result" {
insertAt++
}
rawBlocks = append(rawBlocks, "")
copy(rawBlocks[insertAt+1:], rawBlocks[insertAt:])
rawBlocks[insertAt] = dateBlock
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte("["+strings.Join(rawBlocks, ",")+"]"))
return payload
}
// claudeCodeContextManagement is the context_management object Claude Code
// 2.1.220 sends on every Messages request, captured 2026-08-01 from an isolated
// profile talking to api.anthropic.com. keep:"all" retains every thinking block,
// so replicating the client's exact value cannot produce upstream behaviour the
// real client does not already get.
const claudeCodeContextManagement = `{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}`
// claudeThinkingAcceptsClearThinking reports whether the payload's thinking
// value allows the clear_thinking_20251015 strategy. Anthropic rejects the
// request outright otherwise:
//
// `clear_thinking_20251015` strategy requires `thinking` to be enabled or adaptive
//
// An absent thinking field is therefore just as ineligible as an explicit
// {"type":"disabled"}, which is why this checks for the accepted values rather
// than excluding the disabled one.
func claudeThinkingAcceptsClearThinking(payload []byte) bool {
switch gjson.GetBytes(payload, "thinking.type").String() {
case "enabled", "adaptive":
return true
default:
return false
}
}
// injectClaudeCodeContextManagement supplies context_management when the caller
// omitted it. CPA already claims context-management-2025-06-27 in Anthropic-Beta,
// so a missing body field is an observable inconsistency with the real client. A
// caller that sent its own object keeps it untouched.
func injectClaudeCodeContextManagement(payload []byte) ([]byte, bool) {
if gjson.GetBytes(payload, "context_management").Exists() {
return payload, false
}
if !claudeThinkingAcceptsClearThinking(payload) {
return payload, false
}
updated, err := sjson.SetRawBytes(payload, "context_management", []byte(claudeCodeContextManagement))
if err != nil {
return payload, false
}
return updated, true
}
type claudeCodeContextManagementState struct {
eligible bool
callerOwned bool
automaticallyInjected bool
payloadRuleTouched bool
}
// reconcileClaudeCodeContextManagement resolves automatic ownership after all
// payload rules and forced tool-choice processing have completed.
func reconcileClaudeCodeContextManagement(payload []byte, state claudeCodeContextManagementState) []byte {
contextManagement := gjson.GetBytes(payload, "context_management")
// Any thinking value the strategy does not accept must drop an object CPA
// injected itself. disableThinkingIfToolChoiceForced deletes the whole
// thinking field after injection, so this also covers a request that was
// still eligible when injectClaudeCodeContextManagement ran.
if !claudeThinkingAcceptsClearThinking(payload) {
if state.callerOwned || !state.automaticallyInjected || state.payloadRuleTouched {
return payload
}
if contextManagement.Raw != claudeCodeContextManagement {
return payload
}
updated, err := sjson.DeleteBytes(payload, "context_management")
if err != nil {
return payload
}
return updated
}
if !state.eligible || state.callerOwned || state.payloadRuleTouched || contextManagement.Exists() {
return payload
}
updated, err := sjson.SetRawBytes(payload, "context_management", []byte(claudeCodeContextManagement))
if err != nil {
return payload
}
return updated
}
// withEphemeralCacheControl stamps the native Claude Code default cache marker
// {"type":"ephemeral"} onto a content block. A 1h ttl is not part of the default
// shape; upgradeClaudeCacheControlTTL adds it for the credentials native uses it
// on, after all placement decisions are final.
func withEphemeralCacheControl(rawBlock string) string {
updated, err := sjson.SetRawBytes([]byte(rawBlock), "cache_control", []byte(`{"type":"ephemeral"}`))
if err != nil {
return rawBlock
}
return string(updated)
}
type claudeWirePolicy struct {
OAuth bool // real OAuth token runtime identity
ProfileClaudeCodeCLI bool // request fingerprint looks like Claude Code CLI
ConfirmedClaudeCode bool
Cloak bool
}
type claudeCloakSettings struct {
strictMode bool
sensitiveWords []string
cacheUserID bool
}
func resolveClaudeWirePolicy(cfg *config.Config, auth *cliproxyauth.Auth, apiKey string, confirmedClaudeCode bool) (claudeWirePolicy, claudeCloakSettings) {
cloakCfg := resolveClaudeKeyCloakConfig(cfg, auth)
attrMode, attrStrict, attrWords, attrCache := getCloakConfigFromAuth(auth)
cloakMode := "auto"
if cfg != nil && cfg.DisableClaudeCloakMode {
cloakMode = "never"
}
settings := claudeCloakSettings{
strictMode: attrStrict,
sensitiveWords: attrWords,
cacheUserID: attrCache,
}
if attrMode != "" {
cloakMode = attrMode
}
if cloakCfg != nil {
if mode := strings.TrimSpace(cloakCfg.Mode); mode != "" {
cloakMode = mode
}
if cloakCfg.StrictMode {
settings.strictMode = true
}
if len(cloakCfg.SensitiveWords) > 0 {
settings.sensitiveWords = cloakCfg.SensitiveWords
}
if cloakCfg.CacheUserID != nil {
settings.cacheUserID = *cloakCfg.CacheUserID
}
}
fp := resolveClaudeFingerprintPolicy(cfg, auth, apiKey)
cloakConfigured := cloakCfg != nil || attrMode != "" || attrStrict || len(attrWords) > 0 || attrCache
policy := claudeWirePolicy{
OAuth: fp.AuthIsOAuthToken,
ProfileClaudeCodeCLI: fp.ProfileClaudeCodeCLI,
ConfirmedClaudeCode: confirmedClaudeCode,
Cloak: (fp.ProfileClaudeCodeCLI || cloakConfigured) && !confirmedClaudeCode,
}
if confirmedClaudeCode {
// Native Claude Code is always a passthrough client. An operator-level
// "always" mode may cloak unknown callers, but must not overwrite a
// strongly confirmed CLI, sdk-cli, or claude-vscode fingerprint.
policy.Cloak = false
return policy, settings
}
switch strings.ToLower(strings.TrimSpace(cloakMode)) {
case "always":
policy.Cloak = true
case "never":
policy.Cloak = false
default:
// Auto applies the CLI cloak only to real Claude OAuth credentials,
// explicit fingerprint-profile opt-ins, or credentials with explicit cloak
// settings. Other API keys and delegated providers keep the caller shape.
}
return policy, settings
}
// applyCloaking applies the shared Messages/count_tokens wire policy. The
// returned boolean reports whether cloaking ran.
func applyCloaking(
ctx context.Context,
cfg *config.Config,
auth *cliproxyauth.Auth,
payload []byte,
apiKey string,
confirmedClaudeCode bool,
cchSigning bool,
) ([]byte, bool, error) {
policy, settings := resolveClaudeWirePolicy(cfg, auth, apiKey, confirmedClaudeCode)
if !policy.Cloak {
return payload, false, nil
}
// Strict mode drops caller system prompts entirely, so nothing needs a
// destination and an unusable block cannot lose information.
if !settings.strictMode {
if errSystem := validateClaudeCallerSystemBlocks(gjson.GetBytes(payload, "system")); errSystem != nil {
return nil, false, errSystem
}
}
billingVersion := helps.DefaultClaudeVersion(cfg)
workload := getWorkloadFromContext(ctx)
payload = checkSystemInstructionsWithSigningModeAt(payload, settings.strictMode, cchSigning, billingVersion, "cli", workload, claudeCodeCurrentTime(cfg, auth))
// Claude-Code-CLI fingerprint identity (real OAuth or fingerprint-profile=claude-code-cli)
// is applied later through the shared ApplyClaudeCredentialMetadata path.
// Other non-OAuth cloaking keeps the legacy per-request fake user_id.
if !policy.ProfileClaudeCodeCLI {
var errFakeUserID error
payload, errFakeUserID = injectFakeUserID(ctx, payload, apiKey, settings.cacheUserID)
if errFakeUserID != nil {
return nil, false, errFakeUserID
}
}
// Apply sensitive word obfuscation
if len(settings.sensitiveWords) > 0 {
matcher := helps.BuildSensitiveWordMatcher(settings.sensitiveWords)
payload = helps.ObfuscateSensitiveWords(payload, matcher)
}
return payload, true, nil
}
type claudeCacheControl struct {
Type string `json:"type"`
TTL string `json:"ttl,omitempty"`
}
// claudeCodeCacheControl is the default Claude Code breakpoint shape.
//
// Recovered from the cache-control constructor in the installed 2.1.220,
// 2.1.221 and 2.1.227 binaries, which is byte-identical in all three:
//
// function ctor({scope, ttl} = {}) {
// return {type: "ephemeral", ...ttl && {ttl}, ...scope === "global" && {scope}}
// }
//
// ttl is spread in only when the caller passes one, so the default native wire
// shape carries no ttl at all. upgradeClaudeCacheControlTTL applies the 1h pool
// separately, for the credentials native selects it on. The struct field order
// preserves the native {type, ttl} key order when sjson marshals a value.
var claudeCodeCacheControl = claudeCacheControl{
Type: "ephemeral",
}
// claudeCacheControlTTL1h is the only non-default ttl native ever selects.
const claudeCacheControlTTL1h = "1h"
// ensureCacheControl injects default cache_control breakpoints for translated
// entrypoints (Responses/Chat/Gemini) after cloaking. Placement follows the
// native request builder recovered from the installed binaries:
// 1. LAST system block when no system marker exists
// 2. LAST cacheable message when that message has no marker
//
// Tools are normally not stamped: the native Messages builder never passes a
// cacheControl to its tool-schema converter, and a system breakpoint already
// covers the tools prefix. The one exception is a payload with tools but no
// system at all, which native never produces (it always sends a system prompt).
// Without the fallback such a request has its only breakpoint on the volatile
// final message, so a stateless caller with large tool definitions rewrites the
// whole prefix on every request and never reads it back.
//
// Each section injects independently so cloaking's first-user marker cannot
// suppress system/latest-user breakpoints. Callers still run enforceCacheControlLimit.
// See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
func ensureCacheControl(payload []byte) []byte {
if !claudePayloadHasCacheableSystem(payload) {
payload = injectToolsCacheControl(payload)
}
payload = injectSystemCacheControl(payload)
payload = injectMessagesCacheControl(payload)
return payload
}
// claudePayloadHasCacheableSystem reports whether the payload has a system prompt
// that injectSystemCacheControl can actually host a breakpoint on. An absent key, an
// empty array and an empty string all leave the tools prefix uncovered.
func claudePayloadHasCacheableSystem(payload []byte) bool {
system := gjson.GetBytes(payload, "system")
switch {
case !system.Exists():
return false
case system.IsArray():
return system.Get("#").Int() > 0
case system.Type == gjson.String:
return strings.TrimSpace(system.String()) != ""
default:
return false
}
}
// upgradeClaudeCacheControlTTL mirrors the native ttl upgrade helper, which only
// touches blocks that already carry a cache_control without a ttl:
//
// function upgrade(block, ttl) {
// if (!("cache_control" in block) || !block.cache_control || block.cache_control.ttl) return block
// return {...block, cache_control: {...block.cache_control, ttl}}
// }
//
// It never creates a breakpoint, so placement stays owned by ensureCacheControl.
// Native gates the 1h selection on OAuth scopes, a non-overage account and an
// allowlisted internal query source, and pushes extended-cache-ttl-2025-04-11
// only when that selection produced a 1h body ttl. CPA has no query-source
// equivalent, so the credential check is the reproducible half: OAuth is exactly
// when claudeCodeCLIBetas emits extended-cache-ttl, which keeps body ttl and the
// beta strictly paired the way native does. API-key credentials keep the plain
// {"type":"ephemeral"} native default, which also avoids sending ttl to
// Anthropic-compatible gateways that never advertised support for it.
func upgradeClaudeCacheControlTTL(payload []byte, ttl string) []byte {
if ttl == "" || len(payload) == 0 || !gjson.ValidBytes(payload) {
return payload
}
upgrade := func(path string, block gjson.Result) {
cacheControl := block.Get("cache_control")
if !cacheControl.IsObject() || cacheControl.Get("ttl").Exists() {
return
}
blockType := cacheControl.Get("type")
if blockType.Type != gjson.String {
return
}
// Rebuild the object so the native {type, ttl, scope} key order survives
// instead of appending ttl after a caller-supplied scope.
upgraded := `{"type":` + marshalJSONStringWithoutHTMLEscape(blockType.String()) +
`,"ttl":` + marshalJSONStringWithoutHTMLEscape(ttl)
if scope := cacheControl.Get("scope"); scope.Exists() {
upgraded += `,"scope":` + scope.Raw
}
upgraded += "}"
updated, errSet := sjson.SetRawBytes(payload, path+".cache_control", []byte(upgraded))
if errSet != nil {
return
}
payload = updated
}
forEachClaudeCacheControlBlock(payload, upgrade)
return payload
}
// forEachClaudeCacheControlBlock walks every block that can carry cache_control
// in Anthropic's evaluation order: tools, then system, then messages.
func forEachClaudeCacheControlBlock(payload []byte, visit func(path string, block gjson.Result)) {
if tools := gjson.GetBytes(payload, "tools"); tools.IsArray() {
tools.ForEach(func(idx, item gjson.Result) bool {
visit(fmt.Sprintf("tools.%d", int(idx.Int())), item)
return true
})
}
if system := gjson.GetBytes(payload, "system"); system.IsArray() {
system.ForEach(func(idx, item gjson.Result) bool {
visit(fmt.Sprintf("system.%d", int(idx.Int())), item)
return true
})
}
if messages := gjson.GetBytes(payload, "messages"); messages.IsArray() {
messages.ForEach(func(msgIdx, message gjson.Result) bool {
content := message.Get("content")
if !content.IsArray() {
return true
}
content.ForEach(func(itemIdx, item gjson.Result) bool {
visit(fmt.Sprintf("messages.%d.content.%d", int(msgIdx.Int()), int(itemIdx.Int())), item)
return true
})
return true
})
}
}
func shouldEnsureCacheControl(payload []byte, cloaked, confirmedClaudeCode bool) bool {
return !confirmedClaudeCode && (cloaked || countCacheControls(payload) == 0)
}
func countCacheControls(payload []byte) int {
count := 0
// Check system
system := gjson.GetBytes(payload, "system")
if system.IsArray() {
system.ForEach(func(_, item gjson.Result) bool {
if item.Get("cache_control").Exists() {
count++
}
return true
})
}
// Check tools
tools := gjson.GetBytes(payload, "tools")
if tools.IsArray() {
tools.ForEach(func(_, item gjson.Result) bool {
if item.Get("cache_control").Exists() {
count++
}
return true
})
}
// Check messages
messages := gjson.GetBytes(payload, "messages")
if messages.IsArray() {
messages.ForEach(func(_, msg gjson.Result) bool {
content := msg.Get("content")
if content.IsArray() {
content.ForEach(func(_, item gjson.Result) bool {
if item.Get("cache_control").Exists() {
count++
}
return true
})
}
return true
})
}
return count
}
// normalizeCacheControlTTL ensures cache_control TTL values don't violate the
// prompt-caching-scope-2026-01-05 ordering constraint: a 1h-TTL block must not
// appear after a 5m-TTL block anywhere in the evaluation order.
//
// Anthropic evaluates blocks in order: tools → system (index 0..N) → messages.
// Within each section, blocks are evaluated in array order. A 5m (default) block
// followed by a 1h block at ANY later position is an error — including within
// the same section (e.g. system[1]=5m then system[3]=1h).
//
// Strategy: walk all cache_control blocks in evaluation order. Once a 5m block
// is seen, strip ttl from ALL subsequent 1h blocks (downgrading them to 5m).
func normalizeCacheControlTTL(payload []byte) []byte {
if len(payload) == 0 || !gjson.ValidBytes(payload) {
return payload
}
original := payload
seen5m := false
modified := false
processBlock := func(path string, obj gjson.Result) {
cc := obj.Get("cache_control")
if !cc.Exists() {
return
}
if !cc.IsObject() {
seen5m = true
return
}
ttl := cc.Get("ttl")
if ttl.Type != gjson.String || ttl.String() != "1h" {
seen5m = true
return
}
if !seen5m {
return
}
ttlPath := path + ".cache_control.ttl"
updated, errDel := sjson.DeleteBytes(payload, ttlPath)
if errDel != nil {
return
}
payload = updated
modified = true
}
tools := gjson.GetBytes(payload, "tools")
if tools.IsArray() {
tools.ForEach(func(idx, item gjson.Result) bool {
processBlock(fmt.Sprintf("tools.%d", int(idx.Int())), item)
return true
})
}
system := gjson.GetBytes(payload, "system")
if system.IsArray() {
system.ForEach(func(idx, item gjson.Result) bool {
processBlock(fmt.Sprintf("system.%d", int(idx.Int())), item)
return true
})
}
messages := gjson.GetBytes(payload, "messages")
if messages.IsArray() {
messages.ForEach(func(msgIdx, msg gjson.Result) bool {
content := msg.Get("content")
if !content.IsArray() {
return true
}
content.ForEach(func(itemIdx, item gjson.Result) bool {
processBlock(fmt.Sprintf("messages.%d.content.%d", int(msgIdx.Int()), int(itemIdx.Int())), item)
return true
})
return true
})
}
if !modified {
return original
}
return payload
}
// enforceCacheControlLimit removes excess cache_control blocks from a payload
// so the total does not exceed the Anthropic API limit (currently 4).
//
// Anthropic evaluates cache breakpoints in order: tools → system → messages.
// The most valuable breakpoints are:
// 1. Last tool — caches ALL tool definitions
// 2. Last system block — caches ALL system content
// 3. Recent messages — cache conversation context
//
// Removal priority (strip lowest-value first):
//
// Phase 1: system blocks earliest-first, preserving the last one.
// Phase 2: tool blocks earliest-first, preserving the last one.
// Phase 3: message content blocks earliest-first.
// Phase 4: remaining system blocks (last system).
// Phase 5: remaining tool blocks (last tool).
func enforceCacheControlLimit(payload []byte, maxBlocks int) []byte {
if len(payload) == 0 || !gjson.ValidBytes(payload) {
return payload
}
total := countCacheControls(payload)
if total <= maxBlocks {
return payload
}
excess := total - maxBlocks
system := gjson.GetBytes(payload, "system")
if system.IsArray() {
lastIdx := -1
system.ForEach(func(idx, item gjson.Result) bool {
if item.Get("cache_control").Exists() {
lastIdx = int(idx.Int())
}
return true
})
if lastIdx >= 0 {
system.ForEach(func(idx, item gjson.Result) bool {
if excess <= 0 {
return false
}
i := int(idx.Int())
if i == lastIdx {
return true
}
if !item.Get("cache_control").Exists() {
return true
}
path := fmt.Sprintf("system.%d.cache_control", i)
updated, errDel := sjson.DeleteBytes(payload, path)
if errDel != nil {
return true
}
payload = updated
excess--
return true
})
}
}
if excess <= 0 {
return payload
}
tools := gjson.GetBytes(payload, "tools")
if tools.IsArray() {
lastIdx := -1
tools.ForEach(func(idx, item gjson.Result) bool {
if item.Get("cache_control").Exists() {
lastIdx = int(idx.Int())
}
return true
})
if lastIdx >= 0 {
tools.ForEach(func(idx, item gjson.Result) bool {
if excess <= 0 {
return false
}
i := int(idx.Int())
if i == lastIdx {
return true
}
if !item.Get("cache_control").Exists() {
return true
}
path := fmt.Sprintf("tools.%d.cache_control", i)
updated, errDel := sjson.DeleteBytes(payload, path)
if errDel != nil {
return true
}
payload = updated
excess--
return true
})
}
}
if excess <= 0 {
return payload
}
messages := gjson.GetBytes(payload, "messages")
if messages.IsArray() {
messages.ForEach(func(msgIdx, msg gjson.Result) bool {
if excess <= 0 {
return false
}
content := msg.Get("content")
if !content.IsArray() {
return true
}
content.ForEach(func(itemIdx, item gjson.Result) bool {
if excess <= 0 {
return false
}
if !item.Get("cache_control").Exists() {
return true
}
path := fmt.Sprintf("messages.%d.content.%d.cache_control", int(msgIdx.Int()), int(itemIdx.Int()))
updated, errDel := sjson.DeleteBytes(payload, path)
if errDel != nil {
return true
}
payload = updated
excess--
return true
})
return true
})
}
if excess <= 0 {
return payload
}
system = gjson.GetBytes(payload, "system")
if system.IsArray() {
system.ForEach(func(idx, item gjson.Result) bool {
if excess <= 0 {
return false
}
if !item.Get("cache_control").Exists() {
return true
}
path := fmt.Sprintf("system.%d.cache_control", int(idx.Int()))
updated, errDel := sjson.DeleteBytes(payload, path)
if errDel != nil {
return true
}
payload = updated
excess--
return true
})
}
if excess <= 0 {
return payload
}
tools = gjson.GetBytes(payload, "tools")
if tools.IsArray() {
tools.ForEach(func(idx, item gjson.Result) bool {
if excess <= 0 {
return false
}
if !item.Get("cache_control").Exists() {
return true
}
path := fmt.Sprintf("tools.%d.cache_control", int(idx.Int()))
updated, errDel := sjson.DeleteBytes(payload, path)
if errDel != nil {
return true
}
payload = updated
excess--
return true
})
}
return payload
}
// injectMessagesCacheControl adds cache_control to the message the native rolling
// breakpoint selector would pick. Recovered from the marker selector in the
// installed 2.1.220/2.1.221/2.1.227 binaries:
//
// eligible(msg): a non-assistant turn is always eligible; an assistant turn with
// string content is eligible; an assistant turn with array content
// is eligible only when its last block is not thinking-like.
// last := walk back from the end, skipping internal system turns and
// ineligible turns.
// target := (final turn is a system turn with non-empty STRING content and
// last >= 0) ? final turn : last
//
// The final-system special case is deliberately narrow: native requires string
// content there and writes a brand new single text block for it rather than
// stamping the last element of an existing array. Markers on other messages must
// not suppress this rolling write.
func injectMessagesCacheControl(payload []byte) []byte {
messages := gjson.GetBytes(payload, "messages")
if !messages.Exists() || !messages.IsArray() {
return payload
}
lastMessageIndex := int(messages.Get("#").Int()) - 1
lastEligibleIndex := -1
messages.ForEach(func(index gjson.Result, message gjson.Result) bool {
if role := message.Get("role").String(); role != "user" && role != "assistant" {
return true
}
if claudeMessageEligibleForRollingCache(message) {
lastEligibleIndex = int(index.Int())
}
return true
})
if lastEligibleIndex >= 0 {
finalMessage := messages.Get(fmt.Sprintf("%d", lastMessageIndex))
finalContent := finalMessage.Get("content")
if finalMessage.Get("role").String() == "system" &&
finalContent.Type == gjson.String &&
strings.TrimSpace(finalContent.String()) != "" {
return injectClaudeFinalSystemCacheControl(payload, lastMessageIndex, finalContent.String())
}
}
if lastEligibleIndex < 0 {
return payload
}
contentPath := fmt.Sprintf("messages.%d.content", lastEligibleIndex)
content := gjson.GetBytes(payload, contentPath)
if messageContentHasCacheControl(content) {
return payload
}
if content.IsArray() {
contentCount := int(content.Get("#").Int())
if contentCount > 0 {
cacheControlPath := fmt.Sprintf("messages.%d.content.%d.cache_control", lastEligibleIndex, contentCount-1)
result, err := sjson.SetBytes(payload, cacheControlPath, claudeCodeCacheControl)
if err != nil {
log.Warnf("failed to inject cache_control into messages: %v", err)
return payload
}
payload = result
}
} else if content.Type == gjson.String {
newContent := "[" + buildTextBlock(content.String(), &claudeCodeCacheControl) + "]"
result, err := sjson.SetRawBytes(payload, contentPath, []byte(newContent))
if err != nil {
log.Warnf("failed to inject cache_control into message string content: %v", err)
return payload
}
payload = result
}
return payload
}
// claudeMessageEligibleForRollingCache reports whether the native selector would
// consider this user/assistant turn as a rolling breakpoint host. Native rejects
// an assistant turn whose last content block is thinking-like, because a thinking
// block cannot host the marker.
func claudeMessageEligibleForRollingCache(message gjson.Result) bool {
content := message.Get("content")
if content.Type == gjson.String {
return true
}
if !content.IsArray() || content.Get("#").Int() == 0 {
return false
}
if message.Get("role").String() != "assistant" {
return true
}
lastBlock := content.Get(fmt.Sprintf("%d", content.Get("#").Int()-1))
switch lastBlock.Get("type").String() {
case "thinking", "redacted_thinking":
return false
default:
return true
}
}
// injectClaudeFinalSystemCacheControl reproduces the native final-system special
// case, which replaces the string content with a single marked text block.
func injectClaudeFinalSystemCacheControl(payload []byte, messageIndex int, text string) []byte {
contentPath := fmt.Sprintf("messages.%d.content", messageIndex)
newContent := "[" + buildTextBlock(text, &claudeCodeCacheControl) + "]"
result, err := sjson.SetRawBytes(payload, contentPath, []byte(newContent))
if err != nil {
log.Warnf("failed to inject cache_control into trailing system message: %v", err)
return payload
}
return result
}
func messageContentHasCacheControl(content gjson.Result) bool {
if content.IsArray() {
found := false
content.ForEach(func(_, item gjson.Result) bool {
if item.Get("cache_control").Exists() {
found = true
return false
}
return true
})
return found
}
return false
}
// injectToolsCacheControl adds cache_control to the last non-deferred tool in the tools array.
// Deferred tools cannot use prompt caching, so trailing deferred tools are skipped.
// This only adds cache_control if NO tool in the array already has it.
func injectToolsCacheControl(payload []byte) []byte {
tools := gjson.GetBytes(payload, "tools")
if !tools.Exists() || !tools.IsArray() {
return payload
}
// Check if ANY tool already has cache_control and find the last eligible tool.
hasCacheControlInTools := false
lastEligibleToolIndex := -1
tools.ForEach(func(index, tool gjson.Result) bool {
if tool.Get("cache_control").Exists() {
hasCacheControlInTools = true
return false
}
if !tool.Get("defer_loading").Bool() {
lastEligibleToolIndex = int(index.Int())
}
return true
})
if hasCacheControlInTools || lastEligibleToolIndex < 0 {
return payload
}
lastToolPath := fmt.Sprintf("tools.%d.cache_control", lastEligibleToolIndex)
result, err := sjson.SetBytes(payload, lastToolPath, claudeCodeCacheControl)
if err != nil {
log.Warnf("failed to inject cache_control into tools array: %v", err)
return payload
}
return result
}
// injectSystemCacheControl adds cache_control to the last element in the system prompt.
// Converts string system prompts to array format if needed.
// This only adds cache_control if NO system element already has it.
func injectSystemCacheControl(payload []byte) []byte {
system := gjson.GetBytes(payload, "system")
if !system.Exists() {
return payload
}
if system.IsArray() {
count := int(system.Get("#").Int())
if count == 0 {
return payload
}
// Check if ANY system element already has cache_control
hasCacheControlInSystem := false
system.ForEach(func(_, item gjson.Result) bool {
if item.Get("cache_control").Exists() {
hasCacheControlInSystem = true
return false
}
return true
})
if hasCacheControlInSystem {
return payload
}
// Add cache_control to the last system element
lastSystemPath := fmt.Sprintf("system.%d.cache_control", count-1)
result, err := sjson.SetBytes(payload, lastSystemPath, claudeCodeCacheControl)
if err != nil {
log.Warnf("failed to inject cache_control into system array: %v", err)
return payload
}
payload = result
} else if system.Type == gjson.String {
// Empty/blank strings are not cacheable hosts. claudePayloadHasCacheableSystem
// already treats them as missing so tools can cover the prefix; converting them
// here would create a second, useless breakpoint on whitespace.
if strings.TrimSpace(system.String()) == "" {
return payload
}
// Convert string system prompt to an ordered native text block.
newSystem := "[" + buildTextBlock(system.String(), &claudeCodeCacheControl) + "]"
result, err := sjson.SetRawBytes(payload, "system", []byte(newSystem))
if err != nil {
log.Warnf("failed to inject cache_control into system string: %v", err)
return payload
}
payload = result
}
return payload
}
func ensureModelMaxTokens(body []byte, modelID string) []byte {
if len(body) == 0 || !gjson.ValidBytes(body) {
return body
}
if maxTokens := gjson.GetBytes(body, "max_tokens"); maxTokens.Exists() {
return body
}
for _, provider := range registry.GetGlobalRegistry().GetModelProviders(strings.TrimSpace(modelID)) {
if strings.EqualFold(provider, "claude") {
maxTokens := defaultModelMaxTokens
if info := registry.GetGlobalRegistry().GetModelInfo(strings.TrimSpace(modelID), "claude"); info != nil && info.MaxCompletionTokens > 0 {
maxTokens = info.MaxCompletionTokens
}
body, _ = sjson.SetBytes(body, "max_tokens", maxTokens)
return body
}
}
return body
}