package handlers import ( "cursor-api-proxy/internal/agent" "cursor-api-proxy/internal/anthropic" "cursor-api-proxy/internal/config" "cursor-api-proxy/internal/httputil" "cursor-api-proxy/internal/logger" "cursor-api-proxy/internal/models" "cursor-api-proxy/internal/openai" "cursor-api-proxy/internal/parser" "cursor-api-proxy/internal/pool" "cursor-api-proxy/internal/sanitize" "cursor-api-proxy/internal/winlimit" "cursor-api-proxy/internal/workspace" "encoding/json" "fmt" "net/http" "time" "github.com/google/uuid" ) func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.BridgeConfig, lastModelRef *string, rawBody, method, pathname, remoteAddress string) { var req anthropic.MessagesRequest if err := json.Unmarshal([]byte(rawBody), &req); err != nil { httputil.WriteJSON(w, 400, map[string]interface{}{ "error": map[string]string{"type": "invalid_request_error", "message": "invalid JSON body"}, }, nil) return } requested := openai.NormalizeModelID(req.Model) model := ResolveModel(requested, lastModelRef, cfg) // Parse system from raw body to handle both string and array var rawMap map[string]interface{} _ = json.Unmarshal([]byte(rawBody), &rawMap) cleanSystem := sanitize.SanitizeSystem(req.System) // SanitizeMessages expects []interface{} rawMessages := make([]interface{}, len(req.Messages)) for i, m := range req.Messages { rawMessages[i] = map[string]interface{}{"role": m.Role, "content": m.Content} } cleanRawMessages := sanitize.SanitizeMessages(rawMessages) var cleanMessages []anthropic.MessageParam for _, raw := range cleanRawMessages { if m, ok := raw.(map[string]interface{}); ok { role, _ := m["role"].(string) cleanMessages = append(cleanMessages, anthropic.MessageParam{Role: role, Content: m["content"]}) } } toolsText := openai.ToolsToSystemText(req.Tools, nil) var systemWithTools interface{} if toolsText != "" { sysStr := "" switch v := cleanSystem.(type) { case string: sysStr = v } if sysStr != "" { systemWithTools = sysStr + "\n\n" + toolsText } else { systemWithTools = toolsText } } else { systemWithTools = cleanSystem } prompt := anthropic.BuildPromptFromAnthropicMessages(cleanMessages, systemWithTools) if req.MaxTokens == 0 { httputil.WriteJSON(w, 400, map[string]interface{}{ "error": map[string]string{"type": "invalid_request_error", "message": "max_tokens is required"}, }, nil) return } cursorModel := models.ResolveToCursorModel(model) if cursorModel == "" { cursorModel = model } var trafficMsgs []logger.TrafficMessage if s := systemToString(cleanSystem); s != "" { trafficMsgs = append(trafficMsgs, logger.TrafficMessage{Role: "system", Content: s}) } for _, m := range cleanMessages { text := contentToString(m.Content) if text != "" { trafficMsgs = append(trafficMsgs, logger.TrafficMessage{Role: m.Role, Content: text}) } } logger.LogTrafficRequest(cfg.Verbose, model, trafficMsgs, req.Stream) headerWs := r.Header.Get("x-cursor-workspace") ws := workspace.ResolveWorkspace(cfg, headerWs) fixedArgs := agent.BuildAgentFixedArgs(cfg, ws.WorkspaceDir, cursorModel, req.Stream) fit := winlimit.FitPromptToWinCmdline(cfg.AgentBin, fixedArgs, prompt, cfg.WinCmdlineMax, ws.WorkspaceDir) if !fit.OK { httputil.WriteJSON(w, 500, map[string]interface{}{ "error": map[string]string{"type": "api_error", "message": fit.Error}, }, nil) return } if fit.Truncated { fmt.Printf("[%s] Windows: prompt truncated (%d -> %d chars).\n", time.Now().UTC().Format(time.RFC3339), fit.OriginalLength, fit.FinalPromptLength) } cmdArgs := fit.Args msgID := "msg_" + uuid.New().String() var truncatedHeaders map[string]string if fit.Truncated { truncatedHeaders = map[string]string{"X-Cursor-Proxy-Prompt-Truncated": "true"} } if req.Stream { httputil.WriteSSEHeaders(w, truncatedHeaders) flusher, _ := w.(http.Flusher) writeEvent := func(evt interface{}) { data, _ := json.Marshal(evt) fmt.Fprintf(w, "data: %s\n\n", data) if flusher != nil { flusher.Flush() } } writeEvent(map[string]interface{}{ "type": "message_start", "message": map[string]interface{}{ "id": msgID, "type": "message", "role": "assistant", "model": model, "content": []interface{}{}, }, }) writeEvent(map[string]interface{}{ "type": "content_block_start", "index": 0, "content_block": map[string]string{"type": "text", "text": ""}, }) var accumulated string parseLine := parser.CreateStreamParser( func(text string) { accumulated += text writeEvent(map[string]interface{}{ "type": "content_block_delta", "index": 0, "delta": map[string]string{"type": "text_delta", "text": text}, }) }, func() { logger.LogTrafficResponse(cfg.Verbose, model, accumulated, true) writeEvent(map[string]interface{}{"type": "content_block_stop", "index": 0}) writeEvent(map[string]interface{}{ "type": "message_delta", "delta": map[string]interface{}{"stop_reason": "end_turn", "stop_sequence": nil}, "usage": map[string]int{"output_tokens": 0}, }) writeEvent(map[string]interface{}{"type": "message_stop"}) }, ) configDir := pool.GetNextAccountConfigDir() logger.LogAccountAssigned(configDir) pool.ReportRequestStart(configDir) streamStart := time.Now().UnixMilli() ctx := r.Context() result, err := agent.RunAgentStreamWithContext(cfg, ws.WorkspaceDir, cmdArgs, parseLine, ws.TempDir, configDir, ctx) latencyMs := time.Now().UnixMilli() - streamStart pool.ReportRequestEnd(configDir) if err == nil && isRateLimited(result.Stderr) { pool.ReportRateLimit(configDir, 60000) } if err != nil || result.Code != 0 { pool.ReportRequestError(configDir, latencyMs) if err != nil { logger.LogAgentError(cfg.SessionsLogPath, method, pathname, remoteAddress, -1, err.Error()) } else { logger.LogAgentError(cfg.SessionsLogPath, method, pathname, remoteAddress, result.Code, result.Stderr) } } else { pool.ReportRequestSuccess(configDir, latencyMs) } logger.LogAccountStats(cfg.Verbose, pool.GetAccountStats()) return } configDir := pool.GetNextAccountConfigDir() logger.LogAccountAssigned(configDir) pool.ReportRequestStart(configDir) syncStart := time.Now().UnixMilli() out, err := agent.RunAgentSync(cfg, ws.WorkspaceDir, cmdArgs, ws.TempDir, configDir, r.Context()) syncLatency := time.Now().UnixMilli() - syncStart pool.ReportRequestEnd(configDir) if err != nil { pool.ReportRequestError(configDir, syncLatency) logger.LogAccountStats(cfg.Verbose, pool.GetAccountStats()) httputil.WriteJSON(w, 500, map[string]interface{}{ "error": map[string]string{"type": "api_error", "message": err.Error()}, }, nil) return } if isRateLimited(out.Stderr) { pool.ReportRateLimit(configDir, 60000) } if out.Code != 0 { pool.ReportRequestError(configDir, syncLatency) logger.LogAccountStats(cfg.Verbose, pool.GetAccountStats()) errMsg := logger.LogAgentError(cfg.SessionsLogPath, method, pathname, remoteAddress, out.Code, out.Stderr) httputil.WriteJSON(w, 500, map[string]interface{}{ "error": map[string]string{"type": "api_error", "message": errMsg}, }, nil) return } pool.ReportRequestSuccess(configDir, syncLatency) content := trimSpace(out.Stdout) logger.LogTrafficResponse(cfg.Verbose, model, content, false) logger.LogAccountStats(cfg.Verbose, pool.GetAccountStats()) httputil.WriteJSON(w, 200, map[string]interface{}{ "id": msgID, "type": "message", "role": "assistant", "content": []map[string]string{{"type": "text", "text": content}}, "model": model, "stop_reason": "end_turn", "usage": map[string]int{"input_tokens": 0, "output_tokens": 0}, }, truncatedHeaders) } func systemToString(system interface{}) string { switch v := system.(type) { case string: return v case []interface{}: result := "" for _, p := range v { if m, ok := p.(map[string]interface{}); ok && m["type"] == "text" { if t, ok := m["text"].(string); ok { result += t } } } return result } return "" } func contentToString(content interface{}) string { switch v := content.(type) { case string: return v case []interface{}: result := "" for _, p := range v { if m, ok := p.(map[string]interface{}); ok && m["type"] == "text" { if t, ok := m["text"].(string); ok { result += t } } } return result } return "" }