package executor import ( "bufio" "bytes" "compress/flate" "compress/gzip" "compress/zlib" "context" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" "github.com/andybalholm/brotli" "github.com/google/uuid" "github.com/klauspost/compress/zstd" claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" "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" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "github.com/gin-gonic/gin") const ( claudeTokenCountingBeta = "token-counting-2024-11-01" claudeFastModeBeta = "fast-mode-2026-02-01" claudeOAuthBeta = "oauth-2025-04-20" claudeCodeBeta = "claude-code-20250219" claudeContext1MBeta = "context-1m-2025-08-07" claudeMidConvSystemBeta = "mid-conversation-system-2026-04-07" claudeAdvisorToolBeta = "advisor-tool-2026-03-01" claudeAdvancedToolUseBeta = "advanced-tool-use-2025-11-20" claudeEffortBeta = "effort-2025-11-24" claudeServerSideFallbackBeta = "server-side-fallback-2026-06-01" claudeFallbackCreditBeta = "fallback-credit-2026-06-01" claudeStructuredOutputsBeta = "structured-outputs-2025-12-15" claudeThinkingDisplayUpdatesBeta = "thinking-display-updates-2026-08-18" claudeExtendedCacheTTLBeta = "extended-cache-ttl-2025-04-11" claudeCacheDiagnosisBeta = "cache-diagnosis-2026-04-07" claudeRedactThinkingBeta = "redact-thinking-2026-02-12" claudeAFKModeBeta = "afk-mode-2026-01-31") // claudeCodeCLIConstantBetas are the betas Claude Code sends on every// /v1/messages request from the "cli" entrypoint, in wire order, excluding the// leading claude-code-20250219.//// redact-thinking-2026-02-12 belongs here because cloaked requests always claim// cc_entrypoint=cli; the "sdk-cli" entrypoint omits it. It is still dropped for// requests that carry thinking.display, see claudeThinkingDisplaySet.var claudeCodeCLIConstantBetas = []string{ "interleaved-thinking-2025-05-14", claudeRedactThinkingBeta, "thinking-token-count-2026-05-13", "context-management-2025-06-27", "prompt-caching-scope-2026-01-05",} // claudeCodeTrailingBetas are caller-supplied betas that real Claude Code emits// after effort-2025-11-24, in that relative order. They are forwarded when the// caller asks for them and dropped otherwise.var claudeCodeTrailingBetas = []string{ claudeServerSideFallbackBeta, claudeFallbackCreditBeta, claudeStructuredOutputsBeta,} // claudeManagedBetaSet holds every beta the proxy itself assembles or gates.// Caller betas outside this set are unknown to the pinned Claude Code profile —// newer client releases ship betas past it — and are forwarded verbatim so// their features keep working (#5738).var claudeManagedBetaSet = func() map[string]bool { managed := []string{ claudeTokenCountingBeta, claudeFastModeBeta, claudeOAuthBeta, claudeCodeBeta, claudeContext1MBeta, claudeMidConvSystemBeta, claudeAdvisorToolBeta, claudeAdvancedToolUseBeta, claudeEffortBeta, claudeServerSideFallbackBeta, claudeFallbackCreditBeta, claudeStructuredOutputsBeta, claudeThinkingDisplayUpdatesBeta, claudeExtendedCacheTTLBeta, claudeCacheDiagnosisBeta, claudeRedactThinkingBeta, claudeAFKModeBeta, } managed = append(managed, claudeCodeCLIConstantBetas...) managed = append(managed, claudeCodeTrailingBetas...) set := make(map[string]bool, len(managed)) for _, beta := range managed { set[beta] = true } return set}() func isManagedClaudeBeta(beta string) bool { return claudeManagedBetaSet[strings.TrimSpace(beta)]} // claudeCodeCLIBetas assembles the Anthropic-Beta baseline the way Claude Code// 2.1.258 does: the list is per-request, not a fixed string. requested holds the// betas the caller asked for, which decide the capability flags below.//// Verified against api.anthropic.com with native 2.1.258 captures on interactive,// non-interactive, subagent, and multi-model paths (Sonnet, Opus, Fable, Haiku).// The full observed order is://// 1 claude-code-20250219// 2 oauth-2025-04-20 OAuth credentials only// 3 context-1m-2025-08-07 [1m] model variants only// 4 interleaved-thinking-2025-05-14// 5 redact-thinking-2026-02-12 cli entrypoint, no thinking.display// 6 thinking-token-count-2026-05-13// 7 context-management-2025-06-27// 8 prompt-caching-scope-2026-01-05// 9 mid-conversation-system-2026-04-07 models accepting a role=system turn// 10 advisor-tool-2026-03-01 requests declaring advisor tools or requesting advisor beta// 11 advanced-tool-use-2025-11-20 requests using tool search or another advanced tool-use feature// 12 effort-2025-11-24 effort-supporting models with active thinking// 13 server-side-fallback-2026-06-01 requests with fallbacks or requested// 14 fallback-credit-2026-06-01 OAuth credentials// 15 structured-outputs-2025-12-15 structured output requests// 16 thinking-display-updates-2026-08-18 requests with thinking.display=updates// 17 fast-mode-2026-02-01 speed:fast requests only// 18 afk-mode-2026-01-31 auto-mode sessions, forwarded when the caller sends it// 19 extended-cache-ttl-2025-04-11 OAuth credentials (omitted on subagent & probe)// 20 cache-diagnosis-2026-04-07 requests with diagnostics only//// An empty body keeps the optimistic role=system default, matching the cloaking// policy for unknown and future model IDs.func claudeCodeCLIBetas(body []byte, requested map[string]bool, oauthToken bool) string { betas := make([]string, 0, len(claudeCodeCLIConstantBetas)+len(claudeCodeTrailingBetas)+9) betas = append(betas, claudeCodeBeta) if oauthToken { betas = append(betas, claudeOAuthBeta) } if requested[claudeContext1MBeta] { betas = append(betas, claudeContext1MBeta) } redactThinking := !claudeThinkingDisplaySet(body) for _, beta := range claudeCodeCLIConstantBetas { if beta == claudeRedactThinkingBeta && !redactThinking { continue } betas = append(betas, beta) } if !claudeUsesLegacySystemReminder(body) { betas = append(betas, claudeMidConvSystemBeta) } if requested[claudeAdvisorToolBeta] || claudeBodyHasAdvisorTool(body) { betas = append(betas, claudeAdvisorToolBeta) } if requested[claudeAdvancedToolUseBeta] || claudeBodyUsesAdvancedToolUse(body) { betas = append(betas, claudeAdvancedToolUseBeta) } if claudeRequestSupportsEffort(body, requested) { betas = append(betas, claudeEffortBeta) } isProbeOrHelper := helps.IsClaudeProbeOrHelperRequest(body) if !isProbeOrHelper && (requested[claudeServerSideFallbackBeta] || gjson.GetBytes(body, "fallbacks").Exists()) { betas = append(betas, claudeServerSideFallbackBeta) } if requested[claudeFallbackCreditBeta] || oauthToken { betas = append(betas, claudeFallbackCreditBeta) } for _, beta := range claudeCodeTrailingBetas { if beta == claudeServerSideFallbackBeta || beta == claudeFallbackCreditBeta { continue } if requested[beta] { betas = append(betas, beta) } } thinkingType := gjson.GetBytes(body, "thinking.type").String() if !isProbeOrHelper && thinkingType != "disabled" && (requested[claudeThinkingDisplayUpdatesBeta] || claudeThinkingDisplayUpdates(body)) { betas = append(betas, claudeThinkingDisplayUpdatesBeta) } if claudeRequestUsesFastMode(body, requested) { betas = append(betas, claudeFastModeBeta) } if requested[claudeAFKModeBeta] { betas = append(betas, claudeAFKModeBeta) } if !isProbeOrHelper { includeExtended := (oauthToken && !helps.IsClaudeSubagentRequest(nil, body)) || requested[claudeExtendedCacheTTLBeta] || helps.ClaudePayloadHas1hTTL(body) if includeExtended { betas = append(betas, claudeExtendedCacheTTLBeta) } } if diagnostics := gjson.GetBytes(body, "diagnostics"); diagnostics.IsObject() { betas = append(betas, claudeCacheDiagnosisBeta) } return strings.Join(betas, ",")} func isClaudeHaikuModel(model string) bool { return strings.Contains(strings.ToLower(model), "haiku")} func claudeRequestSupportsEffort(body []byte, requested map[string]bool) bool { if len(body) > 0 { if helps.IsClaudeProbeOrHelperRequest(body) { return false } model := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "model").String())) if isClaudeHaikuModel(model) { return false } thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) if thinkingType == "disabled" { return false } } if requested[claudeEffortBeta] { return true } return true} func claudeThinkingDisplayUpdates(body []byte) bool { display := gjson.GetBytes(body, "thinking.display") return display.Type == gjson.String && strings.EqualFold(strings.TrimSpace(display.String()), "updates")} // claudeBodyUsesAdvancedToolUse reports whether the request needs// advanced-tool-use-2025-11-20. Claude Code 2.1.258 adds the beta only while// tool search is active, which puts a tool_search_tool_* server tool and// defer_loading tools on the wire; plain tool declarations no longer carry it// (measured 2026-09-02: 158 inline tools, no beta). Tool use examples// (input_examples) and programmatic tool calling (allowed_callers) sit behind// the same beta and are just as visible in the body, so callers using them keep// working without requesting the beta explicitly.func claudeBodyUsesAdvancedToolUse(body []byte) bool { tools := gjson.GetBytes(body, "tools") if !tools.IsArray() { return false } for _, tool := range tools.Array() { toolType := strings.ToLower(strings.TrimSpace(tool.Get("type").String())) if strings.HasPrefix(toolType, "tool_search_tool_") { return true } if tool.Get("defer_loading").Bool() || tool.Get("input_examples").Exists() || tool.Get("allowed_callers").Exists() { return true } } return false} // claudeBodyHasAdvisorTool reports whether the request body declares an// advisor server tool.func claudeBodyHasAdvisorTool(body []byte) bool { tools := gjson.GetBytes(body, "tools") if !tools.IsArray() { return false } for _, tool := range tools.Array() { toolType := strings.ToLower(strings.TrimSpace(tool.Get("type").String())) if strings.HasPrefix(toolType, "advisor_") { return true } } return false} // claudeThinkingDisplaySet reports whether the request carries a thinking.display// value. Claude Code 2.1.220 and redact-thinking-2026-02-12 are mutually// exclusive by construction: the beta is only appended while thinking summaries// are off, and the request builder removes it again whenever a display value is// attached. Sending both makes Anthropic honour the redaction and return thinking// blocks with an empty thinking field, so the caller's summary request would be// answered with a signature and no text. Verified on api.anthropic.com with// claude-opus-4-8: display=summarized yields thinking text only when the beta is// absent, and a native 2.1.220 CLI run with showThinkingSummaries enabled sends// display=summarized without the beta.func claudeThinkingDisplaySet(body []byte) bool { display := gjson.GetBytes(body, "thinking.display") return display.Type == gjson.String && strings.TrimSpace(display.String()) != ""} // claudeRequestUsesFastMode reports whether the request selects the fast service// tier. Anthropic rejects the body's speed field with "Extra inputs are not// permitted" unless fast-mode-2026-02-01 is declared, so the beta has to follow// the body. Deriving it here rather than at the call sites is deliberate: the// streaming and non-streaming paths previously disagreed and streaming silently// dropped the beta, turning every fast request into a 400.func claudeRequestUsesFastMode(body []byte, requested map[string]bool) bool { if requested[claudeFastModeBeta] { return true } speed := gjson.GetBytes(body, "speed") return speed.Type == gjson.String && strings.EqualFold(strings.TrimSpace(speed.String()), "fast")} // claudeCountTokensBetas is the fixed profile Claude Code 2.1.220 sends to// /v1/messages/count_tokens. It is far smaller than the inference baseline:// redact-thinking, thinking-token-count, prompt-caching-scope, effort and every// conditional beta are absent. Verified identical across 37 captured calls.var claudeCountTokensBetas = []string{ claudeCodeBeta, "interleaved-thinking-2025-05-14", "context-management-2025-06-27", claudeTokenCountingBeta,} func claudeCountTokensBetasForCredential(oauthToken bool) string { betas := make([]string, 0, len(claudeCountTokensBetas)+1) betas = append(betas, claudeCodeBeta) if oauthToken { betas = append(betas, claudeOAuthBeta) } betas = append(betas, claudeCountTokensBetas[1:]...) return strings.Join(betas, ",")} func withClaudeCountTokensOAuthBeta(betas string) string { parts := make([]string, 0, len(claudeCountTokensBetas)+1) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if seen[claudeOAuthBeta] { return strings.Join(parts, ",") } insertAt := 0 if len(parts) > 0 && parts[0] == claudeCodeBeta { insertAt = 1 } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeOAuthBeta return strings.Join(parts, ",")} // withClaudeOAuthCredentialBetas restores the credential-scoped betas that// describe the selected upstream OAuth account rather than caller capability.//// A confirmed native client authenticates to CPA with whatever key the user// configured and cannot know that CPA will select an OAuth credential upstream,// so its header never carries the OAuth betas. Passing it through verbatim ships// a Bearer request that declares neither oauth-2025-04-20 nor// extended-cache-ttl-2025-04-11, which no real OAuth client ever does. Passthrough// governs what the caller expressed; the credential is CPA's own choice and has to// be described accurately.//// Betas already present are left exactly where the caller put them.func withClaudeOAuthCredentialBetas(betas string, includeExtendedCacheTTL bool) string { parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if !seen[claudeOAuthBeta] { // Captured position 2, directly after claude-code-20250219. insertAt := 0 if len(parts) > 0 && parts[0] == claudeCodeBeta { insertAt = 1 } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeOAuthBeta } if includeExtendedCacheTTL && !seen[claudeExtendedCacheTTLBeta] { parts = append(parts, claudeExtendedCacheTTLBeta) } return strings.Join(parts, ",")} func withoutClaudeBeta(betas, removeBeta string) string { parts := strings.Split(betas, ",") res := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" && p != removeBeta { res = append(res, p) } } return strings.Join(res, ",")} func withClaudeExtendedCacheTTLBeta(betas string) string { parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if !seen[claudeExtendedCacheTTLBeta] { parts = append(parts, claudeExtendedCacheTTLBeta) } return strings.Join(parts, ",")} // withClaudeAdvisorToolBeta ensures advisor-tool-2026-03-01 is present when// the body declares an advisor server tool, placed at the observed wire position// before advanced-tool-use-2025-11-20 or effort-2025-11-24. Every beta that// follows advisor on the wire, afk-mode-2026-01-31 included, is an insertion// boundary so a caller-supplied trailer never ends up ahead of it.func withClaudeAdvisorToolBeta(betas string) string { if strings.TrimSpace(betas) == "" { return claudeAdvisorToolBeta } parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && beta != claudeAdvisorToolBeta && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } insertAt := len(parts) for index, beta := range parts { if beta == claudeAdvancedToolUseBeta || beta == claudeEffortBeta || beta == claudeServerSideFallbackBeta || beta == claudeFallbackCreditBeta || beta == claudeStructuredOutputsBeta || beta == claudeFastModeBeta || beta == claudeAFKModeBeta || beta == claudeExtendedCacheTTLBeta || beta == claudeCacheDiagnosisBeta { insertAt = index break } } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeAdvisorToolBeta return strings.Join(parts, ",")} // claudeEntitlementError marks an upstream refusal that is a property of the// request shape combined with the account's entitlements, not of the credential's// health. The auth manager must neither rotate nor cool down on these.type claudeEntitlementError struct { statusErr} func (claudeEntitlementError) IsRequestScoped() bool { return true} func (claudeEntitlementError) IsCredentialScoped() bool { return false} type claudeRateLimitError struct { statusErr credentialScoped bool} func (e claudeRateLimitError) IsCredentialScoped() bool { return e.credentialScoped} func (e claudeRateLimitError) IsRequestScoped() bool { return false} // classifyClaudeUpstreamError promotes upstream refusals that no other credential// can satisfy into request-scoped errors.//// Anthropic answers a fast-mode request from an account without the matching// usage credits with 429 rate_limit_error "Usage credits are required for fast// mode". The generic pipeline reads 429 as quota exhaustion: it marks the// credential Quota.Exceeded, applies an exponential cooldown and rotates to the// next one, which returns the same 429. A single speed:"fast" request would walk// the whole Claude pool and cool down every credential, all of which remain// perfectly healthy for ordinary traffic. The refusal belongs to the request.func classifyClaudeUpstreamError(statusCode int, headers http.Header, body []byte) error { var retryAfter *time.Duration if statusCode == http.StatusTooManyRequests || (statusCode >= 400 && statusCode < 600) { retryAfter = helps.ParseClaudeRateLimitReset(headers, time.Now())Lines 1–500 of 2514. Load more
package executor import ( "bufio" "bytes" "compress/flate" "compress/gzip" "compress/zlib" "context" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" "github.com/andybalholm/brotli" "github.com/google/uuid" "github.com/klauspost/compress/zstd" claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" "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" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "github.com/gin-gonic/gin" ) const ( claudeTokenCountingBeta = "token-counting-2024-11-01" claudeFastModeBeta = "fast-mode-2026-02-01" claudeOAuthBeta = "oauth-2025-04-20" claudeCodeBeta = "claude-code-20250219" claudeContext1MBeta = "context-1m-2025-08-07" claudeMidConvSystemBeta = "mid-conversation-system-2026-04-07" claudeAdvisorToolBeta = "advisor-tool-2026-03-01" claudeAdvancedToolUseBeta = "advanced-tool-use-2025-11-20" claudeEffortBeta = "effort-2025-11-24" claudeServerSideFallbackBeta = "server-side-fallback-2026-06-01" claudeFallbackCreditBeta = "fallback-credit-2026-06-01" claudeStructuredOutputsBeta = "structured-outputs-2025-12-15" claudeThinkingDisplayUpdatesBeta = "thinking-display-updates-2026-08-18" claudeExtendedCacheTTLBeta = "extended-cache-ttl-2025-04-11" claudeCacheDiagnosisBeta = "cache-diagnosis-2026-04-07" claudeRedactThinkingBeta = "redact-thinking-2026-02-12" claudeAFKModeBeta = "afk-mode-2026-01-31" ) // claudeCodeCLIConstantBetas are the betas Claude Code sends on every // /v1/messages request from the "cli" entrypoint, in wire order, excluding the // leading claude-code-20250219. // // redact-thinking-2026-02-12 belongs here because cloaked requests always claim // cc_entrypoint=cli; the "sdk-cli" entrypoint omits it. It is still dropped for // requests that carry thinking.display, see claudeThinkingDisplaySet. var claudeCodeCLIConstantBetas = []string{ "interleaved-thinking-2025-05-14", claudeRedactThinkingBeta, "thinking-token-count-2026-05-13", "context-management-2025-06-27", "prompt-caching-scope-2026-01-05", } // claudeCodeTrailingBetas are caller-supplied betas that real Claude Code emits // after effort-2025-11-24, in that relative order. They are forwarded when the // caller asks for them and dropped otherwise. var claudeCodeTrailingBetas = []string{ claudeServerSideFallbackBeta, claudeFallbackCreditBeta, claudeStructuredOutputsBeta, } // claudeManagedBetaSet holds every beta the proxy itself assembles or gates. // Caller betas outside this set are unknown to the pinned Claude Code profile — // newer client releases ship betas past it — and are forwarded verbatim so // their features keep working (#5738). var claudeManagedBetaSet = func() map[string]bool { managed := []string{ claudeTokenCountingBeta, claudeFastModeBeta, claudeOAuthBeta, claudeCodeBeta, claudeContext1MBeta, claudeMidConvSystemBeta, claudeAdvisorToolBeta, claudeAdvancedToolUseBeta, claudeEffortBeta, claudeServerSideFallbackBeta, claudeFallbackCreditBeta, claudeStructuredOutputsBeta, claudeThinkingDisplayUpdatesBeta, claudeExtendedCacheTTLBeta, claudeCacheDiagnosisBeta, claudeRedactThinkingBeta, claudeAFKModeBeta, } managed = append(managed, claudeCodeCLIConstantBetas...) managed = append(managed, claudeCodeTrailingBetas...) set := make(map[string]bool, len(managed)) for _, beta := range managed { set[beta] = true } return set }() func isManagedClaudeBeta(beta string) bool { return claudeManagedBetaSet[strings.TrimSpace(beta)] } // claudeCodeCLIBetas assembles the Anthropic-Beta baseline the way Claude Code // 2.1.258 does: the list is per-request, not a fixed string. requested holds the // betas the caller asked for, which decide the capability flags below. // // Verified against api.anthropic.com with native 2.1.258 captures on interactive, // non-interactive, subagent, and multi-model paths (Sonnet, Opus, Fable, Haiku). // The full observed order is: // // 1 claude-code-20250219 // 2 oauth-2025-04-20 OAuth credentials only // 3 context-1m-2025-08-07 [1m] model variants only // 4 interleaved-thinking-2025-05-14 // 5 redact-thinking-2026-02-12 cli entrypoint, no thinking.display // 6 thinking-token-count-2026-05-13 // 7 context-management-2025-06-27 // 8 prompt-caching-scope-2026-01-05 // 9 mid-conversation-system-2026-04-07 models accepting a role=system turn // 10 advisor-tool-2026-03-01 requests declaring advisor tools or requesting advisor beta // 11 advanced-tool-use-2025-11-20 requests using tool search or another advanced tool-use feature // 12 effort-2025-11-24 effort-supporting models with active thinking // 13 server-side-fallback-2026-06-01 requests with fallbacks or requested // 14 fallback-credit-2026-06-01 OAuth credentials // 15 structured-outputs-2025-12-15 structured output requests // 16 thinking-display-updates-2026-08-18 requests with thinking.display=updates // 17 fast-mode-2026-02-01 speed:fast requests only // 18 afk-mode-2026-01-31 auto-mode sessions, forwarded when the caller sends it // 19 extended-cache-ttl-2025-04-11 OAuth credentials (omitted on subagent & probe) // 20 cache-diagnosis-2026-04-07 requests with diagnostics only // // An empty body keeps the optimistic role=system default, matching the cloaking // policy for unknown and future model IDs. func claudeCodeCLIBetas(body []byte, requested map[string]bool, oauthToken bool) string { betas := make([]string, 0, len(claudeCodeCLIConstantBetas)+len(claudeCodeTrailingBetas)+9) betas = append(betas, claudeCodeBeta) if oauthToken { betas = append(betas, claudeOAuthBeta) } if requested[claudeContext1MBeta] { betas = append(betas, claudeContext1MBeta) } redactThinking := !claudeThinkingDisplaySet(body) for _, beta := range claudeCodeCLIConstantBetas { if beta == claudeRedactThinkingBeta && !redactThinking { continue } betas = append(betas, beta) } if !claudeUsesLegacySystemReminder(body) { betas = append(betas, claudeMidConvSystemBeta) } if requested[claudeAdvisorToolBeta] || claudeBodyHasAdvisorTool(body) { betas = append(betas, claudeAdvisorToolBeta) } if requested[claudeAdvancedToolUseBeta] || claudeBodyUsesAdvancedToolUse(body) { betas = append(betas, claudeAdvancedToolUseBeta) } if claudeRequestSupportsEffort(body, requested) { betas = append(betas, claudeEffortBeta) } isProbeOrHelper := helps.IsClaudeProbeOrHelperRequest(body) if !isProbeOrHelper && (requested[claudeServerSideFallbackBeta] || gjson.GetBytes(body, "fallbacks").Exists()) { betas = append(betas, claudeServerSideFallbackBeta) } if requested[claudeFallbackCreditBeta] || oauthToken { betas = append(betas, claudeFallbackCreditBeta) } for _, beta := range claudeCodeTrailingBetas { if beta == claudeServerSideFallbackBeta || beta == claudeFallbackCreditBeta { continue } if requested[beta] { betas = append(betas, beta) } } thinkingType := gjson.GetBytes(body, "thinking.type").String() if !isProbeOrHelper && thinkingType != "disabled" && (requested[claudeThinkingDisplayUpdatesBeta] || claudeThinkingDisplayUpdates(body)) { betas = append(betas, claudeThinkingDisplayUpdatesBeta) } if claudeRequestUsesFastMode(body, requested) { betas = append(betas, claudeFastModeBeta) } if requested[claudeAFKModeBeta] { betas = append(betas, claudeAFKModeBeta) } if !isProbeOrHelper { includeExtended := (oauthToken && !helps.IsClaudeSubagentRequest(nil, body)) || requested[claudeExtendedCacheTTLBeta] || helps.ClaudePayloadHas1hTTL(body) if includeExtended { betas = append(betas, claudeExtendedCacheTTLBeta) } } if diagnostics := gjson.GetBytes(body, "diagnostics"); diagnostics.IsObject() { betas = append(betas, claudeCacheDiagnosisBeta) } return strings.Join(betas, ",") } func isClaudeHaikuModel(model string) bool { return strings.Contains(strings.ToLower(model), "haiku") } func claudeRequestSupportsEffort(body []byte, requested map[string]bool) bool { if len(body) > 0 { if helps.IsClaudeProbeOrHelperRequest(body) { return false } model := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "model").String())) if isClaudeHaikuModel(model) { return false } thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) if thinkingType == "disabled" { return false } } if requested[claudeEffortBeta] { return true } return true } func claudeThinkingDisplayUpdates(body []byte) bool { display := gjson.GetBytes(body, "thinking.display") return display.Type == gjson.String && strings.EqualFold(strings.TrimSpace(display.String()), "updates") } // claudeBodyUsesAdvancedToolUse reports whether the request needs // advanced-tool-use-2025-11-20. Claude Code 2.1.258 adds the beta only while // tool search is active, which puts a tool_search_tool_* server tool and // defer_loading tools on the wire; plain tool declarations no longer carry it // (measured 2026-09-02: 158 inline tools, no beta). Tool use examples // (input_examples) and programmatic tool calling (allowed_callers) sit behind // the same beta and are just as visible in the body, so callers using them keep // working without requesting the beta explicitly. func claudeBodyUsesAdvancedToolUse(body []byte) bool { tools := gjson.GetBytes(body, "tools") if !tools.IsArray() { return false } for _, tool := range tools.Array() { toolType := strings.ToLower(strings.TrimSpace(tool.Get("type").String())) if strings.HasPrefix(toolType, "tool_search_tool_") { return true } if tool.Get("defer_loading").Bool() || tool.Get("input_examples").Exists() || tool.Get("allowed_callers").Exists() { return true } } return false } // claudeBodyHasAdvisorTool reports whether the request body declares an // advisor server tool. func claudeBodyHasAdvisorTool(body []byte) bool { tools := gjson.GetBytes(body, "tools") if !tools.IsArray() { return false } for _, tool := range tools.Array() { toolType := strings.ToLower(strings.TrimSpace(tool.Get("type").String())) if strings.HasPrefix(toolType, "advisor_") { return true } } return false } // claudeThinkingDisplaySet reports whether the request carries a thinking.display // value. Claude Code 2.1.220 and redact-thinking-2026-02-12 are mutually // exclusive by construction: the beta is only appended while thinking summaries // are off, and the request builder removes it again whenever a display value is // attached. Sending both makes Anthropic honour the redaction and return thinking // blocks with an empty thinking field, so the caller's summary request would be // answered with a signature and no text. Verified on api.anthropic.com with // claude-opus-4-8: display=summarized yields thinking text only when the beta is // absent, and a native 2.1.220 CLI run with showThinkingSummaries enabled sends // display=summarized without the beta. func claudeThinkingDisplaySet(body []byte) bool { display := gjson.GetBytes(body, "thinking.display") return display.Type == gjson.String && strings.TrimSpace(display.String()) != "" } // claudeRequestUsesFastMode reports whether the request selects the fast service // tier. Anthropic rejects the body's speed field with "Extra inputs are not // permitted" unless fast-mode-2026-02-01 is declared, so the beta has to follow // the body. Deriving it here rather than at the call sites is deliberate: the // streaming and non-streaming paths previously disagreed and streaming silently // dropped the beta, turning every fast request into a 400. func claudeRequestUsesFastMode(body []byte, requested map[string]bool) bool { if requested[claudeFastModeBeta] { return true } speed := gjson.GetBytes(body, "speed") return speed.Type == gjson.String && strings.EqualFold(strings.TrimSpace(speed.String()), "fast") } // claudeCountTokensBetas is the fixed profile Claude Code 2.1.220 sends to // /v1/messages/count_tokens. It is far smaller than the inference baseline: // redact-thinking, thinking-token-count, prompt-caching-scope, effort and every // conditional beta are absent. Verified identical across 37 captured calls. var claudeCountTokensBetas = []string{ claudeCodeBeta, "interleaved-thinking-2025-05-14", "context-management-2025-06-27", claudeTokenCountingBeta, } func claudeCountTokensBetasForCredential(oauthToken bool) string { betas := make([]string, 0, len(claudeCountTokensBetas)+1) betas = append(betas, claudeCodeBeta) if oauthToken { betas = append(betas, claudeOAuthBeta) } betas = append(betas, claudeCountTokensBetas[1:]...) return strings.Join(betas, ",") } func withClaudeCountTokensOAuthBeta(betas string) string { parts := make([]string, 0, len(claudeCountTokensBetas)+1) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if seen[claudeOAuthBeta] { return strings.Join(parts, ",") } insertAt := 0 if len(parts) > 0 && parts[0] == claudeCodeBeta { insertAt = 1 } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeOAuthBeta return strings.Join(parts, ",") } // withClaudeOAuthCredentialBetas restores the credential-scoped betas that // describe the selected upstream OAuth account rather than caller capability. // // A confirmed native client authenticates to CPA with whatever key the user // configured and cannot know that CPA will select an OAuth credential upstream, // so its header never carries the OAuth betas. Passing it through verbatim ships // a Bearer request that declares neither oauth-2025-04-20 nor // extended-cache-ttl-2025-04-11, which no real OAuth client ever does. Passthrough // governs what the caller expressed; the credential is CPA's own choice and has to // be described accurately. // // Betas already present are left exactly where the caller put them. func withClaudeOAuthCredentialBetas(betas string, includeExtendedCacheTTL bool) string { parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if !seen[claudeOAuthBeta] { // Captured position 2, directly after claude-code-20250219. insertAt := 0 if len(parts) > 0 && parts[0] == claudeCodeBeta { insertAt = 1 } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeOAuthBeta } if includeExtendedCacheTTL && !seen[claudeExtendedCacheTTLBeta] { parts = append(parts, claudeExtendedCacheTTLBeta) } return strings.Join(parts, ",") } func withoutClaudeBeta(betas, removeBeta string) string { parts := strings.Split(betas, ",") res := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" && p != removeBeta { res = append(res, p) } } return strings.Join(res, ",") } func withClaudeExtendedCacheTTLBeta(betas string) string { parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if !seen[claudeExtendedCacheTTLBeta] { parts = append(parts, claudeExtendedCacheTTLBeta) } return strings.Join(parts, ",") } // withClaudeAdvisorToolBeta ensures advisor-tool-2026-03-01 is present when // the body declares an advisor server tool, placed at the observed wire position // before advanced-tool-use-2025-11-20 or effort-2025-11-24. Every beta that // follows advisor on the wire, afk-mode-2026-01-31 included, is an insertion // boundary so a caller-supplied trailer never ends up ahead of it. func withClaudeAdvisorToolBeta(betas string) string { if strings.TrimSpace(betas) == "" { return claudeAdvisorToolBeta } parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && beta != claudeAdvisorToolBeta && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } insertAt := len(parts) for index, beta := range parts { if beta == claudeAdvancedToolUseBeta || beta == claudeEffortBeta || beta == claudeServerSideFallbackBeta || beta == claudeFallbackCreditBeta || beta == claudeStructuredOutputsBeta || beta == claudeFastModeBeta || beta == claudeAFKModeBeta || beta == claudeExtendedCacheTTLBeta || beta == claudeCacheDiagnosisBeta { insertAt = index break } } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeAdvisorToolBeta return strings.Join(parts, ",") } // claudeEntitlementError marks an upstream refusal that is a property of the // request shape combined with the account's entitlements, not of the credential's // health. The auth manager must neither rotate nor cool down on these. type claudeEntitlementError struct { statusErr } func (claudeEntitlementError) IsRequestScoped() bool { return true } func (claudeEntitlementError) IsCredentialScoped() bool { return false } type claudeRateLimitError struct { statusErr credentialScoped bool } func (e claudeRateLimitError) IsCredentialScoped() bool { return e.credentialScoped } func (e claudeRateLimitError) IsRequestScoped() bool { return false } // classifyClaudeUpstreamError promotes upstream refusals that no other credential // can satisfy into request-scoped errors. // // Anthropic answers a fast-mode request from an account without the matching // usage credits with 429 rate_limit_error "Usage credits are required for fast // mode". The generic pipeline reads 429 as quota exhaustion: it marks the // credential Quota.Exceeded, applies an exponential cooldown and rotates to the // next one, which returns the same 429. A single speed:"fast" request would walk // the whole Claude pool and cool down every credential, all of which remain // perfectly healthy for ordinary traffic. The refusal belongs to the request. func classifyClaudeUpstreamError(statusCode int, headers http.Header, body []byte) error { var retryAfter *time.Duration if statusCode == http.StatusTooManyRequests || (statusCode >= 400 && statusCode < 600) { retryAfter = helps.ParseClaudeRateLimitReset(headers, time.Now())