// Copyright (c) 2025-2026 libaxuan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package handlers import ( "github.com/libaxuan/cursor2api-go/config" "github.com/libaxuan/cursor2api-go/middleware" "github.com/libaxuan/cursor2api-go/models" "github.com/libaxuan/cursor2api-go/services" "github.com/libaxuan/cursor2api-go/utils" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) // Handler 处理器结构 type Handler struct { config *config.Config cursorService *services.CursorService docsContent []byte responseStore *services.ResponseStore } // NewHandler 创建新的处理器 func NewHandler(cfg *config.Config) *Handler { cursorService := services.NewCursorService(cfg) // 预加载文档内容 docsPath := "static/docs.html" var docsContent []byte if data, err := os.ReadFile(docsPath); err == nil { docsContent = data } else { // 如果文件不存在,使用默认的简单HTML页面 simpleHTML := ` Cursor2API - Go Version

🚀 Cursor2API - Go Version

Status: ✅ Running

Version: Go Implementation

Description: OpenAI-compatible chat completions proxy for Cursor AI

📡 Available Endpoints:

GET /v1/models
List available AI models
POST /v1/chat/completions
Create chat completion (supports streaming and tool calls)
POST /v1/responses
Create response (Responses API compatible)
GET /health
Health check endpoint

🔐 Authentication:

Use Bearer token authentication:

Authorization: Bearer YOUR_API_KEY

Default API key: 0000 (change via API_KEY environment variable)

💻 Example Usage:

curl -X POST http://localhost:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer 0000" \
  -d '{
    "model": "claude-sonnet-4.6-thinking",
    "messages": [
      {"role": "user", "content": "Plan first, then decide whether a tool is needed."}
    ],
    "stream": true
  }'

Repository: cursor2api-go

Documentation: OpenAI API compatible

` docsContent = []byte(simpleHTML) } return &Handler{ config: cfg, cursorService: cursorService, docsContent: docsContent, responseStore: services.NewResponseStore(0, 0), } } // ListModels 列出可用模型 func (h *Handler) ListModels(c *gin.Context) { modelNames := h.config.GetModels() modelList := make([]models.Model, 0, len(modelNames)) for _, modelID := range modelNames { // 获取模型配置信息 modelConfig, exists := models.GetModelConfig(modelID) model := models.Model{ ID: modelID, Object: "model", Created: time.Now().Unix(), OwnedBy: "cursor2api", } // 如果找到模型配置,添加max_tokens和context_window信息 if exists { model.MaxTokens = modelConfig.MaxTokens model.ContextWindow = modelConfig.ContextWindow } modelList = append(modelList, model) } response := models.ModelsResponse{ Object: "list", Data: modelList, } c.JSON(http.StatusOK, response) } // ChatCompletions 处理聊天完成请求 func (h *Handler) ChatCompletions(c *gin.Context) { var request models.ChatCompletionRequest if err := c.ShouldBindJSON(&request); err != nil { logrus.WithError(err).Error("Failed to bind request") c.JSON(http.StatusBadRequest, models.NewErrorResponse( "Invalid request format", "invalid_request_error", "invalid_json", )) return } // 验证模型 if !h.config.IsValidModel(request.Model) { c.JSON(http.StatusBadRequest, models.NewErrorResponse( "Invalid model specified", "invalid_request_error", "model_not_found", )) return } // 验证消息 if len(request.Messages) == 0 { c.JSON(http.StatusBadRequest, models.NewErrorResponse( "Messages cannot be empty", "invalid_request_error", "missing_messages", )) return } // 验证并调整max_tokens参数 request.MaxTokens = models.ValidateMaxTokens(request.Model, request.MaxTokens) // 调用Cursor服务 // 根据是否流式返回不同响应 if request.Stream { chatGenerator, err := h.cursorService.ChatCompletion(c.Request.Context(), &request) if err != nil { logrus.WithError(err).Error("Failed to create chat completion") middleware.HandleError(c, err) return } utils.SafeStreamWrapper(utils.StreamChatCompletion, c, chatGenerator, request.Model) } else { resp, err := h.cursorService.ChatCompletionNonStream(c.Request.Context(), &request) if err != nil { logrus.WithError(err).Error("Failed to create non-stream chat completion") middleware.HandleError(c, err) return } c.JSON(http.StatusOK, resp) } } // Responses 处理 Responses API 请求 func (h *Handler) Responses(c *gin.Context) { var request models.ResponseRequest if err := c.ShouldBindJSON(&request); err != nil { logrus.WithError(err).Error("Failed to bind response request") c.JSON(http.StatusBadRequest, models.NewErrorResponse( "Invalid request format", "invalid_request_error", "invalid_json", )) return } // 验证模型 if !h.config.IsValidModel(request.Model) { c.JSON(http.StatusBadRequest, models.NewErrorResponse( "Invalid model specified", "invalid_request_error", "model_not_found", )) return } var previousMessages []models.Message previousID := "" if request.PreviousResponseID != nil { previousID = strings.TrimSpace(*request.PreviousResponseID) } if previousID != "" { stored, ok := h.responseStore.Get(previousID) if !ok { c.JSON(http.StatusBadRequest, models.NewErrorResponse( "previous_response_id not found; include prior output items in input instead", "invalid_request_error", "previous_response_id", )) return } previousMessages = stored } chatRequest, adapter, err := services.BuildChatCompletionRequestFromResponse(&request) if err != nil { c.JSON(http.StatusBadRequest, models.NewErrorResponse( err.Error(), "invalid_request_error", "invalid_input", )) return } chatRequest.MaxTokens = models.ValidateMaxTokens(chatRequest.Model, chatRequest.MaxTokens) if len(previousMessages) > 0 { chatRequest.Messages = mergeStoredMessages(chatRequest.Messages, previousMessages) } if chatRequest.Stream { chatGenerator, err := h.cursorService.ChatCompletion(c.Request.Context(), chatRequest) if err != nil { logrus.WithError(err).Error("Failed to create response stream") middleware.HandleError(c, err) return } responseID := utils.GenerateResponseID() accumulator := newStreamAccumulator() wrapped := teeChatGenerator(c.Request.Context(), chatGenerator, accumulator) utils.SafeStreamWrapper(func(ctx *gin.Context, gen <-chan interface{}, model string) { services.StreamResponse(ctx, gen, &request, adapter, responseID) }, c, wrapped, chatRequest.Model) if h.responseStore != nil && accumulator.okToStore() { assistant := accumulator.message() conversation := buildConversationForStore(chatRequest.Messages, assistant) h.responseStore.Put(responseID, conversation) } return } resp, err := h.cursorService.ChatCompletionNonStream(c.Request.Context(), chatRequest) if err != nil { logrus.WithError(err).Error("Failed to create non-stream response") middleware.HandleError(c, err) return } if len(resp.Choices) == 0 { c.JSON(http.StatusInternalServerError, models.NewErrorResponse( "Empty response from upstream", "internal_error", "empty_response", )) return } output, outputText := services.BuildResponseOutputFromMessage(resp.Choices[0].Message, adapter) usage := services.BuildResponseUsage(resp.Usage) response := services.BuildCompletedResponse(&request, output, outputText, usage) if h.responseStore != nil { conversation := buildConversationForStore(chatRequest.Messages, resp.Choices[0].Message) h.responseStore.Put(response.ID, conversation) } c.JSON(http.StatusOK, response) } // ServeDocs 服务API文档页面 func (h *Handler) ServeDocs(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", h.docsContent) } // Health 健康检查 func (h *Handler) Health(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", "timestamp": time.Now().Unix(), "version": "go-1.0.0", }) }