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} // ensureClaudeThinkingDisplay defaults thinking.display to "summarized" when thinking// is active and the client did not set display. Without this, Claude backends that// enable redact-thinking return signature-only thinking blocks (empty thinking text).// Explicit client values such as "omitted" are preserved.func ensureClaudeThinkingDisplay(body []byte) []byte { thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) switch thinkingType { case "enabled", "adaptive", "auto": default: return body } if display := strings.TrimSpace(gjson.GetBytes(body, "thinking.display").String()); display != "" { return body } out, err := sjson.SetBytes(body, "thinking.display", "summarized") if err != nil { return body } return out} 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 Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which // may override it with a user-configured value. Compressed SSE breaks the line // scanner regardless of user preference, so this is non-negotiable for streams. if 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 preservedLines 1–500 of 908. Load more
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-considerations func 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 } // ensureClaudeThinkingDisplay defaults thinking.display to "summarized" when thinking // is active and the client did not set display. Without this, Claude backends that // enable redact-thinking return signature-only thinking blocks (empty thinking text). // Explicit client values such as "omitted" are preserved. func ensureClaudeThinkingDisplay(body []byte) []byte { thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) switch thinkingType { case "enabled", "adaptive", "auto": default: return body } if display := strings.TrimSpace(gjson.GetBytes(body, "thinking.display").String()); display != "" { return body } out, err := sjson.SetBytes(body, "thinking.display", "summarized") if err != nil { return body } return out } 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 Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which // may override it with a user-configured value. Compressed SSE breaks the line // scanner regardless of user preference, so this is non-negotiable for streams. if 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