2514 lines · 86.1 KB · Go
RawDownload
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.