package executor import ( "bufio" "bytes" "context" "fmt" "io" "net/http" "strings" "time" 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" 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/sjson" ) // 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) (cliproxyexecutor.Response, error) { apiKey, baseURL := claudeCreds(auth) if apiKey == "" { return NewClientAdapter("claude").Execute(ctx, auth, req, opts) } if baseURL == "" { baseURL = "https://api.anthropic.com" } from := opts.SourceFormat to := sdktranslator.FromString("claude") body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) if !strings.HasPrefix(req.Model, "claude-3-5-haiku") { body, _ = sjson.SetRawBytes(body, "system", []byte(misc.ClaudeCodeInstructions)) } url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyClaudeHeaders(httpReq, apiKey, false) httpClient := &http.Client{} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { httpClient.Transport = rt } resp, err := httpClient.Do(httpReq) if err != nil { return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) log.Debugf("request error, error status: %d, error body: %s", resp.StatusCode, string(b)) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } data, err := io.ReadAll(resp.Body) if err != nil { return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) return cliproxyexecutor.Response{Payload: []byte(out)}, nil } func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { apiKey, baseURL := claudeCreds(auth) if apiKey == "" { return NewClientAdapter("claude").ExecuteStream(ctx, auth, req, opts) } if baseURL == "" { baseURL = "https://api.anthropic.com" } from := opts.SourceFormat to := sdktranslator.FromString("claude") body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) body, _ = sjson.SetRawBytes(body, "system", []byte(misc.ClaudeCodeInstructions)) url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err } applyClaudeHeaders(httpReq, apiKey, true) httpClient := &http.Client{Timeout: 0} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { httpClient.Transport = rt } resp, err := httpClient.Do(httpReq) if err != nil { return nil, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) log.Debugf("request error, error status: %d, error body: %s", resp.StatusCode, string(b)) return nil, statusErr{code: resp.StatusCode, msg: string(b)} } out := make(chan cliproxyexecutor.StreamChunk) go func() { defer close(out) defer func() { _ = resp.Body.Close() }() scanner := bufio.NewScanner(resp.Body) buf := make([]byte, 1024*1024) scanner.Buffer(buf, 1024*1024) var param any for scanner.Scan() { line := scanner.Bytes() appendAPIResponseChunk(ctx, e.cfg, line) 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 err = scanner.Err(); err != nil { out <- cliproxyexecutor.StreamChunk{Err: err} } }() return 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 } func applyClaudeHeaders(r *http.Request, apiKey string, stream bool) { r.Header.Set("Authorization", "Bearer "+apiKey) r.Header.Set("Content-Type", "application/json") r.Header.Set("Anthropic-Version", "2023-06-01") r.Header.Set("Anthropic-Dangerous-Direct-Browser-Access", "true") r.Header.Set("Anthropic-Beta", "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14") r.Header.Set("Connection", "keep-alive") r.Header.Set("User-Agent", "claude-cli/1.0.83 (external, cli)") r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") r.Header.Set("X-App", "cli") r.Header.Set("X-Stainless-Helper-Method", "stream") r.Header.Set("X-Stainless-Retry-Count", "0") r.Header.Set("X-Stainless-Runtime-Version", "v24.3.0") r.Header.Set("X-Stainless-Package-Version", "0.55.1") r.Header.Set("X-Stainless-Runtime", "node") r.Header.Set("X-Stainless-Lang", "js") r.Header.Set("X-Stainless-Arch", "arm64") r.Header.Set("X-Stainless-Os", "MacOS") r.Header.Set("X-Stainless-Timeout", "60") if stream { r.Header.Set("Accept", "text/event-stream") return } r.Header.Set("Accept", "application/json") } 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 }