887 lines · 29.3 KB · Go
RawDownload
package executor import (	"bufio"	"bytes"	"compress/flate"	"compress/gzip"	"fmt"	"io"	"net/http"	"strings" 	"github.com/andybalholm/brotli"	"github.com/google/uuid"	"github.com/klauspost/compress/zstd"	"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"	"github.com/tidwall/gjson"	"github.com/tidwall/sjson" 	"github.com/gin-gonic/gin") // extractAndRemoveBetas extracts the "betas" array from the body and removes it.// Returns the extracted betas as a string slice and the modified body.func extractAndRemoveBetas(body []byte) ([]string, []byte) {	betasResult := gjson.GetBytes(body, "betas")	if !betasResult.Exists() {		return nil, body	}	var betas []string	if betasResult.IsArray() {		for _, item := range betasResult.Array() {			if s := strings.TrimSpace(item.String()); s != "" {				betas = append(betas, s)			}		}	} else if s := strings.TrimSpace(betasResult.String()); s != "" {		betas = append(betas, s)	}	body, _ = sjson.DeleteBytes(body, "betas")	return betas, body} // disableThinkingIfToolChoiceForced checks if tool_choice forces tool use and disables thinking.// Anthropic API does not allow thinking when tool_choice is set to "any" or a specific tool.// See: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerationsfunc disableThinkingIfToolChoiceForced(body []byte) []byte {	toolChoiceType := gjson.GetBytes(body, "tool_choice.type").String()	// "auto" is allowed with thinking, but "any" or "tool" (specific tool) are not	if toolChoiceType == "any" || toolChoiceType == "tool" {		// Remove thinking configuration entirely to avoid API error		body, _ = sjson.DeleteBytes(body, "thinking")		// Adaptive thinking may also set output_config.effort; remove it to avoid		// leaking thinking controls when tool_choice forces tool use.		body, _ = sjson.DeleteBytes(body, "output_config.effort")		if oc := gjson.GetBytes(body, "output_config"); oc.Exists() && oc.IsObject() && len(oc.Map()) == 0 {			body, _ = sjson.DeleteBytes(body, "output_config")		}	}	return body} // normalizeClaudeSamplingForUpstream keeps Anthropic message requests valid.func normalizeClaudeSamplingForUpstream(body []byte) []byte {	body, _ = sjson.DeleteBytes(body, "temperature")	body, _ = sjson.DeleteBytes(body, "top_p") 	thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String()))	switch thinkingType {	case "enabled", "adaptive", "auto":		body, _ = sjson.DeleteBytes(body, "top_p")		body, _ = sjson.DeleteBytes(body, "top_k")	}	return body} type compositeReadCloser struct {	io.Reader	closers []func() error} func (c *compositeReadCloser) Close() error {	var firstErr error	for i := range c.closers {		if c.closers[i] == nil {			continue		}		if err := c.closers[i](); err != nil && firstErr == nil {			firstErr = err		}	}	return firstErr} // peekableBody wraps a bufio.Reader around the original ReadCloser so that// magic bytes can be inspected without consuming them from the stream.type peekableBody struct {	*bufio.Reader	closer io.Closer} func (p *peekableBody) Close() error {	return p.closer.Close()} func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadCloser, error) {	if body == nil {		return nil, fmt.Errorf("response body is nil")	}	if contentEncoding == "" {		// No Content-Encoding header.  Attempt best-effort magic-byte detection to		// handle misbehaving upstreams that compress without setting the header.		// Only gzip (1f 8b) and zstd (28 b5 2f fd) have reliable magic sequences;		// br and deflate have none and are left as-is.		// The bufio wrapper preserves unread bytes so callers always see the full		// stream regardless of whether decompression was applied.		pb := &peekableBody{Reader: bufio.NewReader(body), closer: body}		magic, peekErr := pb.Peek(4)		if peekErr == nil || (peekErr == io.EOF && len(magic) >= 2) {			switch {			case len(magic) >= 2 && magic[0] == 0x1f && magic[1] == 0x8b:				gzipReader, gzErr := gzip.NewReader(pb)				if gzErr != nil {					_ = pb.Close()					return nil, fmt.Errorf("magic-byte gzip: failed to create reader: %w", gzErr)				}				return &compositeReadCloser{					Reader: gzipReader,					closers: []func() error{						gzipReader.Close,						pb.Close,					},				}, nil			case len(magic) >= 4 && magic[0] == 0x28 && magic[1] == 0xb5 && magic[2] == 0x2f && magic[3] == 0xfd:				decoder, zdErr := zstd.NewReader(pb)				if zdErr != nil {					_ = pb.Close()					return nil, fmt.Errorf("magic-byte zstd: failed to create reader: %w", zdErr)				}				return &compositeReadCloser{					Reader: decoder,					closers: []func() error{						func() error { decoder.Close(); return nil },						pb.Close,					},				}, nil			}		}		return pb, nil	}	encodings := strings.Split(contentEncoding, ",")	for _, raw := range encodings {		encoding := strings.TrimSpace(strings.ToLower(raw))		switch encoding {		case "", "identity":			continue		case "gzip":			gzipReader, err := gzip.NewReader(body)			if err != nil {				_ = body.Close()				return nil, fmt.Errorf("failed to create gzip reader: %w", err)			}			return &compositeReadCloser{				Reader: gzipReader,				closers: []func() error{					gzipReader.Close,					func() error { return body.Close() },				},			}, nil		case "deflate":			deflateReader := flate.NewReader(body)			return &compositeReadCloser{				Reader: deflateReader,				closers: []func() error{					deflateReader.Close,					func() error { return body.Close() },				},			}, nil		case "br":			return &compositeReadCloser{				Reader: brotli.NewReader(body),				closers: []func() error{					func() error { return body.Close() },				},			}, nil		case "zstd":			decoder, err := zstd.NewReader(body)			if err != nil {				_ = body.Close()				return nil, fmt.Errorf("failed to create zstd reader: %w", err)			}			return &compositeReadCloser{				Reader: decoder,				closers: []func() error{					func() error { decoder.Close(); return nil },					func() error { return body.Close() },				},			}, nil		default:			continue		}	}	return body, nil} func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool, extraBetas []string, cfg *config.Config, incomingHeaders http.Header) error {	if r == nil {		return nil	}	hdrDefault := func(cfgVal, fallback string) string {		if cfgVal != "" {			return cfgVal		}		return fallback	} 	var hd config.ClaudeHeaderDefaults	if cfg != nil {		hd = cfg.ClaudeHeaderDefaults	} 	useAPIKey := auth != nil && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["api_key"]) != ""	isAnthropicBase := r.URL != nil && strings.EqualFold(r.URL.Scheme, "https") && strings.EqualFold(r.URL.Host, "api.anthropic.com")	if isAnthropicBase && useAPIKey {		r.Header.Del("Authorization")		r.Header.Set("x-api-key", apiKey)	} else {		r.Header.Set("Authorization", "Bearer "+apiKey)	}	r.Header.Set("Content-Type", "application/json") 	if incomingHeaders == nil {		if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {			incomingHeaders = ginCtx.Request.Header		}	}	stabilizeDeviceProfile := helps.ClaudeDeviceProfileStabilizationEnabled(cfg)	var deviceProfile helps.ClaudeDeviceProfile	if stabilizeDeviceProfile {		var errDeviceProfile error		deviceProfile, errDeviceProfile = helps.ResolveClaudeDeviceProfileRequired(r.Context(), auth, apiKey, incomingHeaders, cfg)		if errDeviceProfile != nil {			return errDeviceProfile		}	} 	baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28"	if val := strings.TrimSpace(strings.Join(incomingHeaders.Values("Anthropic-Beta"), ",")); val != "" {		baseBetas = val		if !strings.Contains(val, "oauth") {			baseBetas += ",oauth-2025-04-20"		}	}	if !strings.Contains(baseBetas, "interleaved-thinking") {		baseBetas += ",interleaved-thinking-2025-05-14"	} 	// Merge extra betas from request body and request flags.	if len(extraBetas) > 0 {		existingSet := make(map[string]bool)		for _, b := range strings.Split(baseBetas, ",") {			betaName := strings.TrimSpace(b)			if betaName != "" {				existingSet[betaName] = true			}		}		for _, beta := range extraBetas {			beta = strings.TrimSpace(beta)			if beta != "" && !existingSet[beta] {				baseBetas += "," + beta				existingSet[beta] = true			}		}	}	r.Header.Set("Anthropic-Beta", baseBetas) 	misc.EnsureHeader(r.Header, incomingHeaders, "Anthropic-Version", "2023-06-01")	// Only set browser access header for API key mode; real Claude Code CLI does not send it.	if useAPIKey {		misc.EnsureHeader(r.Header, incomingHeaders, "Anthropic-Dangerous-Direct-Browser-Access", "true")	}	misc.EnsureHeader(r.Header, incomingHeaders, "X-App", "cli")	// Values below match Claude Code 2.1.63 / @anthropic-ai/sdk 0.74.0 (updated 2026-02-28).	misc.EnsureHeader(r.Header, incomingHeaders, "X-Stainless-Retry-Count", "0")	misc.EnsureHeader(r.Header, incomingHeaders, "X-Stainless-Runtime", "node")	misc.EnsureHeader(r.Header, incomingHeaders, "X-Stainless-Lang", "js")	misc.EnsureHeader(r.Header, incomingHeaders, "X-Stainless-Timeout", hdrDefault(hd.Timeout, "600"))	// Session ID: stable per auth/apiKey, matches Claude Code's X-Claude-Code-Session-Id header.	sessionID, errSessionID := helps.CachedSessionIDRequired(r.Context(), apiKey)	if errSessionID != nil {		return errSessionID	}	misc.EnsureHeader(r.Header, incomingHeaders, "X-Claude-Code-Session-Id", sessionID)	// Per-request UUID, matches Claude Code's x-client-request-id for first-party API.	if isAnthropicBase {		misc.EnsureHeader(r.Header, incomingHeaders, "x-client-request-id", uuid.New().String())	}	r.Header.Set("Connection", "keep-alive")	if stream {		r.Header.Set("Accept", "text/event-stream")		// SSE streams must not be compressed: the downstream scanner reads		// line-delimited text and cannot parse compressed bytes.  Using		// "identity" tells the upstream to send an uncompressed stream.		r.Header.Set("Accept-Encoding", "identity")	} else {		r.Header.Set("Accept", "application/json")		r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")	}	// Legacy mode keeps OS/Arch runtime-derived; stabilized mode pins OS/Arch	// to the configured baseline while still allowing newer official	// User-Agent/package/runtime tuples to upgrade the software fingerprint.	if stabilizeDeviceProfile {		helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile)	} else {		helps.ApplyClaudeLegacyDeviceHeaders(r, incomingHeaders, cfg)	}	var attrs map[string]string	if auth != nil {		attrs = auth.Attributes	}	util.ApplyCustomHeadersFromAttrs(r, attrs)	// Re-enforce the SSE transport contract after custom headers. A custom Accept	// value can disable event negotiation, while compressed SSE breaks line parsing.	if stream {		r.Header.Set("Accept", "text/event-stream")		r.Header.Set("Accept-Encoding", "identity")	}	return nil} func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {	if a == nil {		return "", ""	}	if a.Attributes != nil {		apiKey = a.Attributes["api_key"]		baseURL = a.Attributes["base_url"]	}	if apiKey == "" && a.Metadata != nil {		if v, ok := a.Metadata["access_token"].(string); ok {			apiKey = v		}	}	return} func checkSystemInstructions(payload []byte) []byte {	return checkSystemInstructionsWithSigningMode(payload, false, false, false, "2.1.63", "", "")} func rebuildMidSystemMessagesToTopLevel(payload []byte) []byte {	messages := gjson.GetBytes(payload, "messages")	if !messages.IsArray() {		return payload	} 	var movedSystemParts []string	keptMessages := make([]string, 0, int(messages.Get("#").Int()))	messages.ForEach(func(_, message gjson.Result) bool {		if strings.EqualFold(strings.TrimSpace(message.Get("role").String()), "system") {			movedSystemParts = append(movedSystemParts, claudeSystemTextParts(message.Get("content"))...)			return true		}		keptMessages = append(keptMessages, message.Raw)		return true	})	if len(movedSystemParts) == 0 {		return payload	} 	systemParts := claudeSystemTextParts(gjson.GetBytes(payload, "system"))	systemParts = append(systemParts, movedSystemParts...)	if len(systemParts) > 0 {		if updated, errSetSystem := sjson.SetRawBytes(payload, "system", rawJSONArray(systemParts)); errSetSystem == nil {			payload = updated		}	}	if updated, errSetMessages := sjson.SetRawBytes(payload, "messages", rawJSONArray(keptMessages)); errSetMessages == nil {		payload = updated	}	return payload} func claudeSystemTextParts(content gjson.Result) []string {	if !content.Exists() {		return nil	}	if content.Type == gjson.String {		text := content.String()		if strings.TrimSpace(text) == "" {			return nil		}		block := []byte(`{"type":"text","text":""}`)		block, _ = sjson.SetBytes(block, "text", text)		return []string{string(block)}	}	if !content.IsArray() {		return nil	} 	var parts []string	content.ForEach(func(_, item gjson.Result) bool {		if item.Type == gjson.String {			text := item.String()			if strings.TrimSpace(text) != "" {				block := []byte(`{"type":"text","text":""}`)				block, _ = sjson.SetBytes(block, "text", text)				parts = append(parts, string(block))			}			return true		}		if item.IsObject() && item.Get("type").String() == "text" && strings.TrimSpace(item.Get("text").String()) != "" {			parts = append(parts, item.Raw)		}		return true	})	return parts} func rawJSONArray(items []string) []byte {	if len(items) == 0 {		return []byte("[]")	}	var builder strings.Builder	builder.WriteByte('[')	for i, item := range items {		if i > 0 {			builder.WriteByte(',')		}		builder.WriteString(item)	}	builder.WriteByte(']')	return []byte(builder.String())} func isClaudeOAuthToken(apiKey string) bool {	return strings.Contains(apiKey, "sk-ant-oat")} // prepareClaudeOAuthToolNamesForUpstream applies the Claude OAuth tool-name// transforms in the same order across request paths. Remap runs before prefixing// so any future non-empty prefix still composes correctly with the per-request// reverse map.func prepareClaudeOAuthToolNamesForUpstream(body []byte, prefix string, prefixDisabled bool) ([]byte, map[string]string) {	body, reverseMap := remapOAuthToolNames(body)	if !prefixDisabled {		body = applyClaudeToolPrefix(body, prefix)	}	return body, reverseMap} // restoreClaudeOAuthToolNamesFromResponse undoes the Claude OAuth tool-name// transforms for non-stream responses in reverse order.func restoreClaudeOAuthToolNamesFromResponse(body []byte, prefix string, prefixDisabled bool, reverseMap map[string]string) []byte {	if !prefixDisabled {		body = stripClaudeToolPrefixFromResponse(body, prefix)	}	return reverseRemapOAuthToolNames(body, reverseMap)} // restoreClaudeOAuthToolNamesFromStreamLine undoes the Claude OAuth tool-name// transforms for SSE lines in reverse order.func restoreClaudeOAuthToolNamesFromStreamLine(line []byte, prefix string, prefixDisabled bool, reverseMap map[string]string) []byte {	if !prefixDisabled {		line = stripClaudeToolPrefixFromStreamLine(line, prefix)	}	return reverseRemapOAuthToolNamesFromStreamLine(line, reverseMap)} // remapOAuthToolNames renames third-party tool names to Claude Code equivalents// and removes tools without an official counterpart. This prevents Anthropic from// fingerprinting the request as a third-party client via tool naming patterns.//// It operates on: tools[].name, tool_choice.name, and all tool_use/tool_reference// references in messages. Removed tools' corresponding tool_result blocks are preserved// (they just become orphaned, which is safe for Claude).//// The returned map is keyed on the upstream (TitleCase) name and maps to the// client-supplied original name. Callers MUST pass this map to the reverse// functions so only names the client actually caused us to rewrite are restored// on the response. A global reverse map (the previous implementation) incorrectly// rewrote names the client originally sent in TitleCase (e.g. `Bash`)// when any OTHER tool in the same request triggered a forward rename (e.g.// `glob` -> `Glob`), because the global reverse map contained `Bash` -> `bash`// regardless of what the client originally sent.func remapOAuthToolNames(body []byte) ([]byte, map[string]string) {	reverseMap := make(map[string]string, len(oauthToolRenameMap))	recordRename := func(original, renamed string) {		// Preserve the first-seen original name if the same upstream name is		// produced from multiple call sites; they all map back identically.		if _, exists := reverseMap[renamed]; !exists {			reverseMap[renamed] = original		}	} 	// 1. Rewrite tools array in a single pass (if present).

Lines 1–500 of 887.