2026-04-03 09:15:35 +00:00
|
|
|
// Code scaffolded by goctl. Safe to edit.
|
|
|
|
|
// goctl 1.10.1
|
|
|
|
|
|
|
|
|
|
package chat
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-04-03 15:00:18 +00:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
2026-04-03 09:15:35 +00:00
|
|
|
|
2026-04-03 15:00:18 +00:00
|
|
|
"cursor-api-proxy/internal/config"
|
2026-04-03 09:15:35 +00:00
|
|
|
"cursor-api-proxy/internal/svc"
|
2026-04-03 15:00:18 +00:00
|
|
|
apitypes "cursor-api-proxy/internal/types"
|
|
|
|
|
"cursor-api-proxy/pkg/adapter/openai"
|
|
|
|
|
"cursor-api-proxy/pkg/domain/types"
|
|
|
|
|
"cursor-api-proxy/pkg/infrastructure/logger"
|
|
|
|
|
"cursor-api-proxy/pkg/infrastructure/parser"
|
|
|
|
|
"cursor-api-proxy/pkg/usecase"
|
2026-04-03 09:15:35 +00:00
|
|
|
|
2026-04-03 15:00:18 +00:00
|
|
|
"github.com/google/uuid"
|
2026-04-03 09:15:35 +00:00
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ChatCompletionsLogic struct {
|
|
|
|
|
logx.Logger
|
|
|
|
|
ctx context.Context
|
|
|
|
|
svcCtx *svc.ServiceContext
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewChatCompletionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ChatCompletionsLogic {
|
|
|
|
|
return &ChatCompletionsLogic{
|
|
|
|
|
Logger: logx.WithContext(ctx),
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
svcCtx: svcCtx,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 15:00:18 +00:00
|
|
|
func (l *ChatCompletionsLogic) ChatCompletions(req *apitypes.ChatCompletionRequest) (*apitypes.ChatCompletionResponse, error) {
|
|
|
|
|
cfg := configToBridge(l.svcCtx.Config)
|
|
|
|
|
model := openai.NormalizeModelID(req.Model)
|
|
|
|
|
cursorModel := types.ResolveToCursorModel(model)
|
|
|
|
|
if cursorModel == "" {
|
|
|
|
|
cursorModel = model
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
messages := convertMessages(req.Messages)
|
|
|
|
|
tools := convertTools(req.Tools)
|
|
|
|
|
functions := convertFunctions(req.Functions)
|
|
|
|
|
|
|
|
|
|
cleanMessages := usecase.SanitizeMessages(messages)
|
|
|
|
|
toolsText := openai.ToolsToSystemText(tools, functions)
|
|
|
|
|
messagesWithTools := cleanMessages
|
|
|
|
|
if toolsText != "" {
|
|
|
|
|
messagesWithTools = append([]interface{}{
|
|
|
|
|
map[string]interface{}{"role": "system", "content": toolsText},
|
|
|
|
|
}, cleanMessages...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prompt := openai.BuildPromptFromMessages(messagesWithTools)
|
|
|
|
|
|
|
|
|
|
// TODO: implement non-streaming execution
|
|
|
|
|
_ = cfg
|
|
|
|
|
_ = cursorModel
|
|
|
|
|
_ = prompt
|
|
|
|
|
|
|
|
|
|
return &apitypes.ChatCompletionResponse{
|
|
|
|
|
Id: "chatcmpl_" + uuid.New().String(),
|
|
|
|
|
Object: "chat.completion",
|
|
|
|
|
Created: 0,
|
|
|
|
|
Model: model,
|
|
|
|
|
Choices: []apitypes.Choice{
|
|
|
|
|
{
|
|
|
|
|
Index: 0,
|
|
|
|
|
Message: apitypes.RespMessage{
|
|
|
|
|
Role: "assistant",
|
|
|
|
|
Content: "TODO: implement non-streaming response",
|
|
|
|
|
},
|
|
|
|
|
FinishReason: "stop",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *ChatCompletionsLogic) ChatCompletionsStream(req *apitypes.ChatCompletionRequest, w http.ResponseWriter) error {
|
|
|
|
|
cfg := configToBridge(l.svcCtx.Config)
|
|
|
|
|
model := openai.NormalizeModelID(req.Model)
|
|
|
|
|
cursorModel := types.ResolveToCursorModel(model)
|
|
|
|
|
if cursorModel == "" {
|
|
|
|
|
cursorModel = model
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
messages := convertMessages(req.Messages)
|
|
|
|
|
tools := convertTools(req.Tools)
|
|
|
|
|
functions := convertFunctions(req.Functions)
|
|
|
|
|
|
|
|
|
|
cleanMessages := usecase.SanitizeMessages(messages)
|
|
|
|
|
toolsText := openai.ToolsToSystemText(tools, functions)
|
|
|
|
|
messagesWithTools := cleanMessages
|
|
|
|
|
if toolsText != "" {
|
|
|
|
|
messagesWithTools = append([]interface{}{
|
|
|
|
|
map[string]interface{}{"role": "system", "content": toolsText},
|
|
|
|
|
}, cleanMessages...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prompt := openai.BuildPromptFromMessages(messagesWithTools)
|
|
|
|
|
|
|
|
|
|
if l.svcCtx.Config.Verbose {
|
|
|
|
|
logger.LogDebug("model=%s prompt_len=%d", cursorModel, len(prompt))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id := "chatcmpl_" + uuid.New().String()
|
|
|
|
|
created := int64(0)
|
|
|
|
|
|
|
|
|
|
hasTools := len(tools) > 0 || len(functions) > 0
|
|
|
|
|
|
|
|
|
|
flusher, _ := w.(http.Flusher)
|
|
|
|
|
|
|
|
|
|
var accumulated string
|
|
|
|
|
var chunkNum int
|
|
|
|
|
var p parser.Parser
|
|
|
|
|
|
|
|
|
|
// TODO: implement proper streaming with usecase.RunAgentStream
|
|
|
|
|
// For now, return a placeholder response
|
|
|
|
|
_ = cfg
|
|
|
|
|
_ = prompt
|
|
|
|
|
_ = hasTools
|
|
|
|
|
_ = p
|
|
|
|
|
_ = chunkNum
|
|
|
|
|
_ = accumulated
|
|
|
|
|
|
|
|
|
|
chunk := map[string]interface{}{
|
|
|
|
|
"id": id, "object": "chat.completion.chunk", "created": created, "model": model,
|
|
|
|
|
"choices": []map[string]interface{}{
|
|
|
|
|
{"index": 0, "delta": map[string]string{"content": "Streaming not yet implemented"}, "finish_reason": nil},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
data, _ := json.Marshal(chunk)
|
|
|
|
|
fmt.Fprintf(w, "data: %s\n\n", data)
|
|
|
|
|
if flusher != nil {
|
|
|
|
|
flusher.Flush()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopChunk := map[string]interface{}{
|
|
|
|
|
"id": id, "object": "chat.completion.chunk", "created": created, "model": model,
|
|
|
|
|
"choices": []map[string]interface{}{
|
|
|
|
|
{"index": 0, "delta": map[string]interface{}{}, "finish_reason": "stop"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
data, _ = json.Marshal(stopChunk)
|
|
|
|
|
fmt.Fprintf(w, "data: %s\n\n", data)
|
|
|
|
|
fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
|
|
|
if flusher != nil {
|
|
|
|
|
flusher.Flush()
|
|
|
|
|
}
|
2026-04-03 09:15:35 +00:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-04-03 15:00:18 +00:00
|
|
|
|
|
|
|
|
// Stub implementations - TODO: full implementation
|
|
|
|
|
func convertMessages(msgs []apitypes.Message) []interface{} {
|
|
|
|
|
result := make([]interface{}, len(msgs))
|
|
|
|
|
for i, m := range msgs {
|
|
|
|
|
result[i] = map[string]interface{}{
|
|
|
|
|
"role": m.Role,
|
|
|
|
|
"content": m.Content,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func convertTools(tools []apitypes.Tool) []interface{} {
|
|
|
|
|
if tools == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
result := make([]interface{}, len(tools))
|
|
|
|
|
for i, t := range tools {
|
|
|
|
|
result[i] = map[string]interface{}{
|
|
|
|
|
"type": t.Type,
|
|
|
|
|
"function": map[string]interface{}{
|
|
|
|
|
"name": t.Function.Name,
|
|
|
|
|
"description": t.Function.Description,
|
|
|
|
|
"parameters": t.Function.Parameters,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func convertFunctions(funcs []apitypes.Function) []interface{} {
|
|
|
|
|
if funcs == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
result := make([]interface{}, len(funcs))
|
|
|
|
|
for i, f := range funcs {
|
|
|
|
|
result[i] = map[string]interface{}{
|
|
|
|
|
"name": f.Name,
|
|
|
|
|
"description": f.Description,
|
|
|
|
|
"parameters": f.Parameters,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func configToBridge(c config.Config) config.BridgeConfig {
|
|
|
|
|
return config.BridgeConfig{
|
|
|
|
|
AgentBin: c.AgentBin,
|
|
|
|
|
Host: c.Host,
|
|
|
|
|
Port: c.Port,
|
|
|
|
|
RequiredKey: c.RequiredKey,
|
|
|
|
|
DefaultModel: c.DefaultModel,
|
|
|
|
|
Mode: "ask",
|
|
|
|
|
Provider: c.Provider,
|
|
|
|
|
Force: c.Force,
|
|
|
|
|
ApproveMcps: c.ApproveMcps,
|
|
|
|
|
StrictModel: c.StrictModel,
|
|
|
|
|
Workspace: c.Workspace,
|
|
|
|
|
TimeoutMs: c.TimeoutMs,
|
|
|
|
|
TLSCertPath: c.TLSCertPath,
|
|
|
|
|
TLSKeyPath: c.TLSKeyPath,
|
|
|
|
|
SessionsLogPath: c.SessionsLogPath,
|
|
|
|
|
ChatOnlyWorkspace: c.ChatOnlyWorkspace,
|
|
|
|
|
Verbose: c.Verbose,
|
|
|
|
|
MaxMode: c.MaxMode,
|
|
|
|
|
ConfigDirs: c.ConfigDirs,
|
|
|
|
|
MultiPort: c.MultiPort,
|
|
|
|
|
WinCmdlineMax: c.WinCmdlineMax,
|
|
|
|
|
GeminiAccountDir: c.GeminiAccountDir,
|
|
|
|
|
GeminiBrowserVisible: c.GeminiBrowserVisible,
|
|
|
|
|
GeminiMaxSessions: c.GeminiMaxSessions,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Placeholder for usecase functions
|
|
|
|
|
// These should be properly implemented with the usecase package
|
|
|
|
|
var _ = usecase.AccountsDir
|