package executor import ( "context" "crypto/sha256" "encoding/hex" "fmt" "strings" "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" 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" ) // getClientUserAgent extracts the client User-Agent from the gin context. func getClientUserAgent(ctx context.Context) string { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { return ginCtx.GetHeader("User-Agent") } return "" } // parseEntrypointFromUA extracts the entrypoint from a Claude Code User-Agent. // Format: "claude-cli/x.y.z (external, cli)" → "cli" // Format: "claude-cli/x.y.z (external, vscode)" → "vscode" // Returns "cli" if parsing fails or UA is not Claude Code. func parseEntrypointFromUA(userAgent string) string { // Find content inside parentheses start := strings.Index(userAgent, "(") end := strings.LastIndex(userAgent, ")") if start < 0 || end <= start { return "cli" } inner := userAgent[start+1 : end] // Split by comma, take the second part (entrypoint is at index 1, after USER_TYPE) // Format: "(USER_TYPE, ENTRYPOINT[, extra...])" parts := strings.Split(inner, ",") if len(parts) >= 2 { ep := strings.TrimSpace(parts[1]) if ep != "" { return ep } } return "cli" } // 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 auth.Metadata != nil { if value, ok := auth.Metadata[key].(string); ok { 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) } return helps.GenerateFakeUserID(), 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 // real Claude Code prepends to every system prompt array. // Format: x-anthropic-billing-header: cc_version=.; cc_entrypoint=; cch=; [cc_workload=;] func generateBillingHeader(payload []byte, experimentalCCHSigning 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 experimentalCCHSigning { return fmt.Sprintf("x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=%s; cch=00000;%s", version, buildHash, entrypoint, workloadPart) } // Generate a deterministic cch hash from the payload content (system + messages + tools). h := sha256.Sum256(payload) cch := hex.EncodeToString(h[:])[:5] return fmt.Sprintf("x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=%s; cch=%s;%s", version, buildHash, entrypoint, cch, workloadPart) } func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte { return checkSystemInstructionsWithSigningMode(payload, strictMode, false, false, "2.1.63", "", "") } // checkSystemInstructionsWithSigningMode injects Claude Code-style system blocks: // // system[0]: billing header (no cache_control) // system[1]: agent identifier (cache_control ephemeral, scope=org) // system[2]: core intro prompt (cache_control ephemeral, scope=global) // system[3]: system instructions (no cache_control) // system[4]: doing tasks (no cache_control) // system[5]: user system messages moved to first user message func checkSystemInstructionsWithSigningMode(payload []byte, strictMode bool, experimentalCCHSigning bool, oauthMode bool, version, entrypoint, workload string) []byte { system := gjson.GetBytes(payload, "system") // Extract original message text for fingerprint computation (before billing injection). // Use the first system text block's content as the fingerprint source. messageText := "" if system.IsArray() { system.ForEach(func(_, part gjson.Result) bool { if part.Get("type").String() == "text" { messageText = part.Get("text").String() return false } return true }) } else if system.Type == gjson.String { messageText = system.String() } // Skip if already injected firstText := gjson.GetBytes(payload, "system.0.text").String() if strings.HasPrefix(firstText, "x-anthropic-billing-header:") { return payload } billingText := generateBillingHeader(payload, experimentalCCHSigning, version, messageText, entrypoint, workload) billingBlock := buildTextBlock(billingText, nil) // Build system blocks matching real Claude Code structure. // Important: Claude Code's internal cacheScope='org' does NOT serialize to // scope='org' in the API request. Only scope='global' is sent explicitly. // The system prompt prefix block is sent without cache_control. agentBlock := buildTextBlock("You are Claude Code, Anthropic's official CLI for Claude.", nil) staticPrompt := strings.Join([]string{ helps.ClaudeCodeIntro, helps.ClaudeCodeSystem, helps.ClaudeCodeDoingTasks, helps.ClaudeCodeToneAndStyle, helps.ClaudeCodeOutputEfficiency, }, "\n\n") staticBlock := buildTextBlock(staticPrompt, nil) systemResult := "[" + billingBlock + "," + agentBlock + "," + staticBlock + "]" payload, _ = sjson.SetRawBytes(payload, "system", []byte(systemResult)) // Collect user system instructions and prepend to first user message if !strictMode { var userSystemParts []string if system.IsArray() { system.ForEach(func(_, part gjson.Result) bool { if part.Get("type").String() == "text" { txt := strings.TrimSpace(part.Get("text").String()) if txt != "" { userSystemParts = append(userSystemParts, txt) } } return true }) } else if system.Type == gjson.String && strings.TrimSpace(system.String()) != "" { userSystemParts = append(userSystemParts, strings.TrimSpace(system.String())) } if len(userSystemParts) > 0 { combined := strings.Join(userSystemParts, "\n\n") if oauthMode { combined = sanitizeForwardedSystemPrompt(combined) } if strings.TrimSpace(combined) != "" { payload = prependToFirstUserMessage(payload, combined) } } } return payload } // sanitizeForwardedSystemPrompt reduces forwarded third-party system context to a // tiny neutral reminder for Claude OAuth cloaking. The goal is to preserve only // the minimum tool/task guidance while removing virtually all client-specific // prompt structure that Anthropic may classify as third-party agent traffic. func sanitizeForwardedSystemPrompt(text string) string { if strings.TrimSpace(text) == "" { return "" } return strings.TrimSpace(`Use the available tools when needed to help with software engineering tasks. Keep responses concise and focused on the user's request. Prefer acting on the user's task over describing product-specific workflows.`) } // buildTextBlock constructs a JSON text block object with proper escaping. // Uses sjson.SetBytes to handle multi-line text, quotes, and control characters. // cacheControl is optional; pass nil to omit cache_control. func buildTextBlock(text string, cacheControl map[string]string) string { block := []byte(`{"type":"text"}`) block, _ = sjson.SetBytes(block, "text", text) if cacheControl != nil && len(cacheControl) > 0 { // Build cache_control JSON manually to avoid sjson map marshaling issues. // sjson.SetBytes with map[string]string may not produce expected structure. cc := `{"type":"ephemeral"` if t, ok := cacheControl["ttl"]; ok { cc += fmt.Sprintf(`,"ttl":"%s"`, t) } cc += "}" block, _ = sjson.SetRawBytes(block, "cache_control", []byte(cc)) } return string(block) } // prependToFirstUserMessage injects text content into the first user message. // This avoids putting non-Claude-Code system instructions in system[] which // triggers Anthropic's extra usage billing for OAuth-proxied requests. func prependToFirstUserMessage(payload []byte, text string) []byte { messages := gjson.GetBytes(payload, "messages") if !messages.Exists() || !messages.IsArray() { return payload } // Find the first user message index firstUserIdx := -1 messages.ForEach(func(idx, msg gjson.Result) bool { if msg.Get("role").String() == "user" { firstUserIdx = int(idx.Int()) return false } return true }) if firstUserIdx < 0 { return payload } prefixBlock := fmt.Sprintf(` As you answer the user's questions, you can use the following context from the system: %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. `, text) contentPath := fmt.Sprintf("messages.%d.content", firstUserIdx) content := gjson.GetBytes(payload, contentPath) if content.IsArray() { newBlock := fmt.Sprintf(`{"type":"text","text":%q}`, prefixBlock) var newArray string switch { case content.Raw == "[]" || content.Raw == "": newArray = "[" + newBlock + "]" case leadsWithToolResult(content): // Anthropic requires the user message that immediately follows an // assistant tool_use turn to lead with its tool_result blocks. // Append the reminder so those blocks stay at the head. if trimmed := strings.TrimRight(content.Raw, " \t\r\n"); strings.HasSuffix(trimmed, "]") { newArray = trimmed[:len(trimmed)-1] + "," + newBlock + "]" } else { newArray = "[" + newBlock + "," + content.Raw[1:] } default: newArray = "[" + newBlock + "," + content.Raw[1:] } payload, _ = sjson.SetRawBytes(payload, contentPath, []byte(newArray)) } else if content.Type == gjson.String { newText := prefixBlock + content.String() payload, _ = sjson.SetBytes(payload, contentPath, newText) } return payload } // leadsWithToolResult reports whether a message content array starts with a // tool_result block. Such a message answers a preceding assistant tool_use turn, // and Anthropic requires its tool_result blocks to remain first. func leadsWithToolResult(content gjson.Result) bool { first := content.Get("0") return first.Exists() && first.Get("type").String() == "tool_result" } // applyCloaking applies cloaking transformations to the payload based on config and client. // Cloaking includes: system prompt injection, fake user ID, and sensitive word obfuscation. func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte, model string, apiKey string) ([]byte, error) { clientUserAgent := getClientUserAgent(ctx) // Enable cch signing for OAuth tokens by default (not just experimental flag). oauthToken := isClaudeOAuthToken(apiKey) useCCHSigning := oauthToken || experimentalCCHSigningEnabled(cfg, auth) // Get cloak config from ClaudeKey configuration cloakCfg := resolveClaudeKeyCloakConfig(cfg, auth) attrMode, attrStrict, attrWords, attrCache := getCloakConfigFromAuth(auth) // Determine cloak settings. Precedence (low -> high): // built-in "auto" default // -> global disable-claude-cloak-mode switch (forces "never") // -> per-credential settings from auth attributes/metadata // -> per claude-api-key cloak config cloakMode := "auto" if cfg != nil && cfg.DisableClaudeCloakMode { cloakMode = "never" } 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 { strictMode = true } if len(cloakCfg.SensitiveWords) > 0 { sensitiveWords = cloakCfg.SensitiveWords } if cloakCfg.CacheUserID != nil { cacheUserID = *cloakCfg.CacheUserID } } // Determine if cloaking should be applied if !helps.ShouldCloak(cloakMode, clientUserAgent) { return payload, nil } // Skip system instructions for claude-3-5-haiku models if !strings.HasPrefix(model, "claude-3-5-haiku") { billingVersion := helps.DefaultClaudeVersion(cfg) entrypoint := parseEntrypointFromUA(clientUserAgent) workload := getWorkloadFromContext(ctx) payload = checkSystemInstructionsWithSigningMode(payload, strictMode, useCCHSigning, oauthToken, billingVersion, entrypoint, workload) } // Inject fake user ID var errFakeUserID error payload, errFakeUserID = injectFakeUserID(ctx, payload, apiKey, cacheUserID) if errFakeUserID != nil { return nil, errFakeUserID } // Apply sensitive word obfuscation if len(sensitiveWords) > 0 { matcher := helps.BuildSensitiveWordMatcher(sensitiveWords) payload = helps.ObfuscateSensitiveWords(payload, matcher) } return payload, nil } // ensureCacheControl injects cache_control breakpoints into the payload for optimal prompt caching. // According to Anthropic's documentation, cache prefixes are created in order: tools -> system -> messages. // This function adds cache_control to: // 1. The LAST non-deferred tool in the tools array (caches all preceding tool definitions) // 2. The LAST system prompt element // 3. The SECOND-TO-LAST user turn (caches conversation history for multi-turn) // // Up to 4 cache breakpoints are allowed per request. Tools, System, and Messages are INDEPENDENT breakpoints. // This enables up to 90% cost reduction on cached tokens (cache read = 0.1x base price). // See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching func ensureCacheControl(payload []byte) []byte { // 1. Inject cache_control into the LAST non-deferred tool // Tools are cached first in the hierarchy, so this is the most important breakpoint. payload = injectToolsCacheControl(payload) // 2. Inject cache_control into the LAST system prompt element // System is the second level in the cache hierarchy. payload = injectSystemCacheControl(payload) // 3. Inject cache_control into messages for multi-turn conversation caching // This caches the conversation history up to the second-to-last user turn. payload = injectMessagesCacheControl(payload) return payload } 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 second-to-last user turn for multi-turn caching. // Per Anthropic docs: "Place cache_control on the second-to-last User message to let the model reuse the earlier cache." // This enables caching of conversation history, which is especially beneficial for long multi-turn conversations. // Only adds cache_control if: // - There are at least 2 user turns in the conversation // - No message content already has cache_control func injectMessagesCacheControl(payload []byte) []byte { messages := gjson.GetBytes(payload, "messages") if !messages.Exists() || !messages.IsArray() { return payload } // Check if ANY message content already has cache_control hasCacheControlInMessages := false 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() { hasCacheControlInMessages = true return false } return true }) } return !hasCacheControlInMessages }) if hasCacheControlInMessages { return payload } // Find all user message indices var userMsgIndices []int messages.ForEach(func(index gjson.Result, msg gjson.Result) bool { if msg.Get("role").String() == "user" { userMsgIndices = append(userMsgIndices, int(index.Int())) } return true }) // Need at least 2 user turns to cache the second-to-last if len(userMsgIndices) < 2 { return payload } // Get the second-to-last user message index secondToLastUserIdx := userMsgIndices[len(userMsgIndices)-2] // Get the content of this message contentPath := fmt.Sprintf("messages.%d.content", secondToLastUserIdx) content := gjson.GetBytes(payload, contentPath) if content.IsArray() { // Add cache_control to the last content block of this message contentCount := int(content.Get("#").Int()) if contentCount > 0 { cacheControlPath := fmt.Sprintf("messages.%d.content.%d.cache_control", secondToLastUserIdx, contentCount-1) result, err := sjson.SetBytes(payload, cacheControlPath, map[string]string{"type": "ephemeral"}) if err != nil { log.Warnf("failed to inject cache_control into messages: %v", err) return payload } payload = result } } else if content.Type == gjson.String { // Convert string content to array with cache_control text := content.String() newContent := []map[string]interface{}{ { "type": "text", "text": text, "cache_control": map[string]string{ "type": "ephemeral", }, }, } result, err := sjson.SetBytes(payload, contentPath, newContent) if err != nil { log.Warnf("failed to inject cache_control into message string content: %v", err) return payload } payload = result } return payload } // 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, map[string]string{"type": "ephemeral"}) 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, map[string]string{"type": "ephemeral"}) 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 { // Convert string system prompt to array with cache_control // "system": "text" -> "system": [{"type": "text", "text": "text", "cache_control": {"type": "ephemeral"}}] text := system.String() newSystem := []map[string]interface{}{ { "type": "text", "text": text, "cache_control": map[string]string{ "type": "ephemeral", }, }, } result, err := sjson.SetBytes(payload, "system", 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 }