package executor import ( "bufio" "bytes" "compress/flate" "compress/gzip" "context" "fmt" "io" "net/http" "strings" "time" "github.com/andybalholm/brotli" "github.com/klauspost/compress/zstd" claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "github.com/gin-gonic/gin" ) // ClaudeExecutor is a stateless executor for Anthropic Claude over the messages API. // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. type ClaudeExecutor struct { cfg *config.Config } func NewClaudeExecutor(cfg *config.Config) *ClaudeExecutor { return &ClaudeExecutor{cfg: cfg} } func (e *ClaudeExecutor) Identifier() string { return "claude" } func (e *ClaudeExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) error { return nil } func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { apiKey, baseURL := claudeCreds(auth) if baseURL == "" { baseURL = "https://api.anthropic.com" } reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) defer reporter.trackFailure(ctx, &err) model := req.Model if override := e.resolveUpstreamModel(req.Model, auth); override != "" { model = override } from := opts.SourceFormat to := sdktranslator.FromString("claude") // Use streaming translation to preserve function calling, except for claude. stream := from != to originalPayload := bytes.Clone(req.Payload) if len(opts.OriginalRequest) > 0 { originalPayload = bytes.Clone(opts.OriginalRequest) } originalTranslated := sdktranslator.TranslateRequest(from, to, model, originalPayload, stream) body := sdktranslator.TranslateRequest(from, to, model, bytes.Clone(req.Payload), stream) body, _ = sjson.SetBytes(body, "model", model) // Inject thinking config based on model metadata for thinking variants body = e.injectThinkingConfig(model, req.Metadata, body) if !strings.HasPrefix(model, "claude-3-5-haiku") { body = checkSystemInstructions(body) } body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated) // Disable thinking if tool_choice forces tool use (Anthropic API constraint) body = disableThinkingIfToolChoiceForced(body) // Ensure max_tokens > thinking.budget_tokens when thinking is enabled body = ensureMaxTokensForThinking(model, body) // Extract betas from body and convert to header var extraBetas []string extraBetas, body = extractAndRemoveBetas(body) url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return resp, err } applyClaudeHeaders(httpReq, auth, apiKey, false, extraBetas) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID authLabel = auth.Label authType, authValue = auth.AccountInfo() } recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), Body: body, Provider: e.Identifier(), AuthID: authID, AuthLabel: authLabel, AuthType: authType, AuthValue: authValue, }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { recordAPIResponseError(ctx, e.cfg, err) return resp, err } recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { b, _ := io.ReadAll(httpResp.Body) appendAPIResponseChunk(ctx, e.cfg, b) log.Warnf("claude executor: upstream API error, status: %d, body: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) err = statusErr{code: httpResp.StatusCode, msg: string(b)} if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } return resp, err } decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) if err != nil { recordAPIResponseError(ctx, e.cfg, err) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } return resp, err } defer func() { if errClose := decodedBody.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } }() data, err := io.ReadAll(decodedBody) if err != nil { recordAPIResponseError(ctx, e.cfg, err) return resp, err } appendAPIResponseChunk(ctx, e.cfg, data) if stream { lines := bytes.Split(data, []byte("\n")) for _, line := range lines { if detail, ok := parseClaudeStreamUsage(line); ok { reporter.publish(ctx, detail) } } } else { reporter.publish(ctx, parseClaudeUsage(data)) } var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) resp = cliproxyexecutor.Response{Payload: []byte(out)} return resp, nil } func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) { apiKey, baseURL := claudeCreds(auth) if baseURL == "" { baseURL = "https://api.anthropic.com" } reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) defer reporter.trackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("claude") model := req.Model if override := e.resolveUpstreamModel(req.Model, auth); override != "" { model = override } originalPayload := bytes.Clone(req.Payload) if len(opts.OriginalRequest) > 0 { originalPayload = bytes.Clone(opts.OriginalRequest) } originalTranslated := sdktranslator.TranslateRequest(from, to, model, originalPayload, true) body := sdktranslator.TranslateRequest(from, to, model, bytes.Clone(req.Payload), true) body, _ = sjson.SetBytes(body, "model", model) // Inject thinking config based on model metadata for thinking variants body = e.injectThinkingConfig(model, req.Metadata, body) body = checkSystemInstructions(body) body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated) // Disable thinking if tool_choice forces tool use (Anthropic API constraint) body = disableThinkingIfToolChoiceForced(body) // Ensure max_tokens > thinking.budget_tokens when thinking is enabled body = ensureMaxTokensForThinking(model, body) // Extract betas from body and convert to header var extraBetas []string extraBetas, body = extractAndRemoveBetas(body) url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err } applyClaudeHeaders(httpReq, auth, apiKey, true, extraBetas) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID authLabel = auth.Label authType, authValue = auth.AccountInfo() } recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), Body: body, Provider: e.Identifier(), AuthID: authID, AuthLabel: authLabel, AuthType: authType, AuthValue: authValue, }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { recordAPIResponseError(ctx, e.cfg, err) return nil, err } recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { b, _ := io.ReadAll(httpResp.Body) appendAPIResponseChunk(ctx, e.cfg, b) log.Warnf("claude executor: upstream API error, status: %d, body: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } err = statusErr{code: httpResp.StatusCode, msg: string(b)} return nil, err } decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) if err != nil { recordAPIResponseError(ctx, e.cfg, err) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } return nil, err } out := make(chan cliproxyexecutor.StreamChunk) stream = out go func() { defer close(out) defer func() { if errClose := decodedBody.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } }() // If from == to (Claude → Claude), directly forward the SSE stream without translation if from == to { scanner := bufio.NewScanner(decodedBody) scanner.Buffer(nil, 52_428_800) // 50MB for scanner.Scan() { line := scanner.Bytes() appendAPIResponseChunk(ctx, e.cfg, line) if detail, ok := parseClaudeStreamUsage(line); ok { reporter.publish(ctx, detail) } // Forward the line as-is to preserve SSE format cloned := make([]byte, len(line)+1) copy(cloned, line) cloned[len(line)] = '\n' out <- cliproxyexecutor.StreamChunk{Payload: cloned} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) reporter.publishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } return } // For other formats, use translation scanner := bufio.NewScanner(decodedBody) scanner.Buffer(nil, 52_428_800) // 50MB var param any for scanner.Scan() { line := scanner.Bytes() appendAPIResponseChunk(ctx, e.cfg, line) if detail, ok := parseClaudeStreamUsage(line); ok { reporter.publish(ctx, detail) } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) for i := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} } } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) reporter.publishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } }() return stream, nil } func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { apiKey, baseURL := claudeCreds(auth) if baseURL == "" { baseURL = "https://api.anthropic.com" } from := opts.SourceFormat to := sdktranslator.FromString("claude") // Use streaming translation to preserve function calling, except for claude. stream := from != to model := req.Model if override := e.resolveUpstreamModel(req.Model, auth); override != "" { model = override } body := sdktranslator.TranslateRequest(from, to, model, bytes.Clone(req.Payload), stream) body, _ = sjson.SetBytes(body, "model", model) if !strings.HasPrefix(model, "claude-3-5-haiku") { body = checkSystemInstructions(body) } // Extract betas from body and convert to header (for count_tokens too) var extraBetas []string extraBetas, body = extractAndRemoveBetas(body) url := fmt.Sprintf("%s/v1/messages/count_tokens?beta=true", baseURL) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyClaudeHeaders(httpReq, auth, apiKey, false, extraBetas) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID authLabel = auth.Label authType, authValue = auth.AccountInfo() } recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), Body: body, Provider: e.Identifier(), AuthID: authID, AuthLabel: authLabel, AuthType: authType, AuthValue: authValue, }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) if errClose := resp.Body.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } decodedBody, err := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding")) if err != nil { recordAPIResponseError(ctx, e.cfg, err) if errClose := resp.Body.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } return cliproxyexecutor.Response{}, err } defer func() { if errClose := decodedBody.Close(); errClose != nil { log.Errorf("response body close error: %v", errClose) } }() data, err := io.ReadAll(decodedBody) if err != nil { recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) count := gjson.GetBytes(data, "input_tokens").Int() out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) return cliproxyexecutor.Response{Payload: []byte(out)}, nil } func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { log.Debugf("claude executor: refresh called") if auth == nil { return nil, fmt.Errorf("claude executor: auth is nil") } var refreshToken string if auth.Metadata != nil { if v, ok := auth.Metadata["refresh_token"].(string); ok && v != "" { refreshToken = v } } if refreshToken == "" { return auth, nil } svc := claudeauth.NewClaudeAuth(e.cfg) td, err := svc.RefreshTokens(ctx, refreshToken) if err != nil { return nil, err } if auth.Metadata == nil { auth.Metadata = make(map[string]any) } auth.Metadata["access_token"] = td.AccessToken if td.RefreshToken != "" { auth.Metadata["refresh_token"] = td.RefreshToken } auth.Metadata["email"] = td.Email auth.Metadata["expired"] = td.Expire auth.Metadata["type"] = "claude" now := time.Now().Format(time.RFC3339) auth.Metadata["last_refresh"] = now return auth, nil } // 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 } // injectThinkingConfig adds thinking configuration based on metadata using the unified flow. // It uses util.ResolveClaudeThinkingConfig which internally calls ResolveThinkingConfigFromMetadata // and NormalizeThinkingBudget, ensuring consistency with other executors like Gemini. func (e *ClaudeExecutor) injectThinkingConfig(modelName string, metadata map[string]any, body []byte) []byte { budget, ok := util.ResolveClaudeThinkingConfig(modelName, metadata) if !ok { return body } return util.ApplyClaudeThinkingConfig(body, budget) } // 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") } return body } // ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled. // Anthropic API requires this constraint; violating it returns a 400 error. // This function should be called after all thinking configuration is finalized. // It looks up the model's MaxCompletionTokens from the registry to use as the cap. func ensureMaxTokensForThinking(modelName string, body []byte) []byte { thinkingType := gjson.GetBytes(body, "thinking.type").String() if thinkingType != "enabled" { return body } budgetTokens := gjson.GetBytes(body, "thinking.budget_tokens").Int() if budgetTokens <= 0 { return body } maxTokens := gjson.GetBytes(body, "max_tokens").Int() // Look up the model's max completion tokens from the registry maxCompletionTokens := 0 if modelInfo := registry.GetGlobalRegistry().GetModelInfo(modelName); modelInfo != nil { maxCompletionTokens = modelInfo.MaxCompletionTokens } // Fall back to budget + buffer if registry lookup fails or returns 0 const fallbackBuffer = 4000 requiredMaxTokens := budgetTokens + fallbackBuffer if maxCompletionTokens > 0 { requiredMaxTokens = int64(maxCompletionTokens) } if maxTokens < requiredMaxTokens { body, _ = sjson.SetBytes(body, "max_tokens", requiredMaxTokens) } return body } func (e *ClaudeExecutor) resolveUpstreamModel(alias string, auth *cliproxyauth.Auth) string { trimmed := strings.TrimSpace(alias) if trimmed == "" { return "" } entry := e.resolveClaudeConfig(auth) if entry == nil { return "" } normalizedModel, metadata := util.NormalizeThinkingModel(trimmed) // Candidate names to match against configured aliases/names. candidates := []string{strings.TrimSpace(normalizedModel)} if !strings.EqualFold(normalizedModel, trimmed) { candidates = append(candidates, trimmed) } if original := util.ResolveOriginalModel(normalizedModel, metadata); original != "" && !strings.EqualFold(original, normalizedModel) { candidates = append(candidates, original) } for i := range entry.Models { model := entry.Models[i] name := strings.TrimSpace(model.Name) modelAlias := strings.TrimSpace(model.Alias) for _, candidate := range candidates { if candidate == "" { continue } if modelAlias != "" && strings.EqualFold(modelAlias, candidate) { if name != "" { return name } return candidate } if name != "" && strings.EqualFold(name, candidate) { return name } } } return "" } func (e *ClaudeExecutor) resolveClaudeConfig(auth *cliproxyauth.Auth) *config.ClaudeKey { if auth == nil || e.cfg == nil { return nil } var attrKey, attrBase string if auth.Attributes != nil { attrKey = strings.TrimSpace(auth.Attributes["api_key"]) attrBase = strings.TrimSpace(auth.Attributes["base_url"]) } for i := range e.cfg.ClaudeKey { entry := &e.cfg.ClaudeKey[i] cfgKey := strings.TrimSpace(entry.APIKey) cfgBase := strings.TrimSpace(entry.BaseURL) if attrKey != "" && attrBase != "" { if strings.EqualFold(cfgKey, attrKey) && strings.EqualFold(cfgBase, attrBase) { return entry } continue } if attrKey != "" && strings.EqualFold(cfgKey, attrKey) { if cfgBase == "" || strings.EqualFold(cfgBase, attrBase) { return entry } } if attrKey == "" && attrBase != "" && strings.EqualFold(cfgBase, attrBase) { return entry } } if attrKey != "" { for i := range e.cfg.ClaudeKey { entry := &e.cfg.ClaudeKey[i] if strings.EqualFold(strings.TrimSpace(entry.APIKey), attrKey) { return entry } } } return nil } 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 } func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadCloser, error) { if body == nil { return nil, fmt.Errorf("response body is nil") } if contentEncoding == "" { return body, 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) { 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") var ginHeaders http.Header if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { ginHeaders = ginCtx.Request.Header } baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" if val := strings.TrimSpace(ginHeaders.Get("Anthropic-Beta")); val != "" { baseBetas = val if !strings.Contains(val, "oauth") { baseBetas += ",oauth-2025-04-20" } } // Merge extra betas from request body if len(extraBetas) > 0 { existingSet := make(map[string]bool) for _, b := range strings.Split(baseBetas, ",") { existingSet[strings.TrimSpace(b)] = 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, ginHeaders, "Anthropic-Version", "2023-06-01") misc.EnsureHeader(r.Header, ginHeaders, "Anthropic-Dangerous-Direct-Browser-Access", "true") misc.EnsureHeader(r.Header, ginHeaders, "X-App", "cli") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Helper-Method", "stream") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Retry-Count", "0") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Runtime-Version", "v24.3.0") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Package-Version", "0.55.1") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Runtime", "node") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Lang", "js") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Arch", "arm64") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Os", "MacOS") misc.EnsureHeader(r.Header, ginHeaders, "X-Stainless-Timeout", "60") misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", "claude-cli/1.0.83 (external, cli)") r.Header.Set("Connection", "keep-alive") r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") if stream { r.Header.Set("Accept", "text/event-stream") } else { r.Header.Set("Accept", "application/json") } var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(r, attrs) } 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 { system := gjson.GetBytes(payload, "system") claudeCodeInstructions := `[{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude."}]` if system.IsArray() { if gjson.GetBytes(payload, "system.0.text").String() != "You are Claude Code, Anthropic's official CLI for Claude." { system.ForEach(func(_, part gjson.Result) bool { if part.Get("type").String() == "text" { claudeCodeInstructions, _ = sjson.SetRaw(claudeCodeInstructions, "-1", part.Raw) } return true }) payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions)) } } else { payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions)) } return payload }