// 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
}'
`
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",
})
}