package services import ( "bytes" "github.com/libaxuan/cursor2api-go/middleware" "github.com/libaxuan/cursor2api-go/models" "github.com/libaxuan/cursor2api-go/utils" "encoding/json" "fmt" "strings" ) const thinkingHint = "Use ... for hidden reasoning when it helps. Keep your final visible answer outside the thinking tags." type cursorBuildResult struct { Payload models.CursorRequest ParseConfig models.CursorParseConfig } type toolChoiceSpec struct { Mode string FunctionName string } func (s *CursorService) buildCursorRequest(request *models.ChatCompletionRequest) (cursorBuildResult, error) { capability := models.ResolveModelCapability(request.Model) toolChoice, err := parseToolChoice(request.ToolChoice) if err != nil { return cursorBuildResult{}, middleware.NewRequestValidationError(err.Error(), "invalid_tool_choice") } // Kilo Code 兼容:当上层编排器希望“必须用工具”时,即便 tool_choice=auto, // 也可以通过环境变量强制要求至少一次工具调用,避免 MODEL_NO_TOOLS_USED 一类的上层报错。 if s.config != nil && s.config.KiloToolStrict && len(request.Tools) > 0 && toolChoice.Mode == "auto" { toolChoice.Mode = "required" } if len(request.Tools) == 0 && toolChoice.Mode != "auto" && toolChoice.Mode != "none" { return cursorBuildResult{}, middleware.NewRequestValidationError("tool_choice requires tools to be provided", "missing_tools") } if err := validateTools(request.Tools); err != nil { return cursorBuildResult{}, middleware.NewRequestValidationError(err.Error(), "invalid_tools") } if toolChoice.FunctionName != "" && !toolExists(request.Tools, toolChoice.FunctionName) { return cursorBuildResult{}, middleware.NewRequestValidationError( fmt.Sprintf("tool_choice references unknown function %q", toolChoice.FunctionName), "unknown_tool_choice_function", ) } hasToolHistory := messagesContainToolHistory(request.Messages) toolProtocolEnabled := len(request.Tools) > 0 && toolChoice.Mode != "none" triggerSignal := "" if toolProtocolEnabled || hasToolHistory { triggerSignal = "<>" } cursorMessages := buildCursorMessages( request.Messages, s.config.SystemPromptInject, request.Tools, toolChoice, capability, hasToolHistory, triggerSignal, ) cursorMessages = s.truncateCursorMessages(cursorMessages) payload := models.CursorRequest{ Context: []interface{}{}, Model: models.GetCursorModel(request.Model), ID: utils.GenerateRandomString(16), Messages: cursorMessages, Trigger: "submit-message", } return cursorBuildResult{ Payload: payload, ParseConfig: models.CursorParseConfig{ TriggerSignal: triggerSignal, ThinkingEnabled: capability.ThinkingEnabled, }, }, nil } func parseToolChoice(raw json.RawMessage) (toolChoiceSpec, error) { if len(bytes.TrimSpace(raw)) == 0 || string(bytes.TrimSpace(raw)) == "null" { return toolChoiceSpec{Mode: "auto"}, nil } var choiceString string if err := json.Unmarshal(raw, &choiceString); err == nil { switch choiceString { case "auto", "none", "required": return toolChoiceSpec{Mode: choiceString}, nil default: return toolChoiceSpec{}, fmt.Errorf("unsupported tool_choice value %q", choiceString) } } var choiceObject models.ToolChoiceObject if err := json.Unmarshal(raw, &choiceObject); err != nil { return toolChoiceSpec{}, fmt.Errorf("tool_choice must be a string or function object") } if choiceObject.Type != "function" { return toolChoiceSpec{}, fmt.Errorf("unsupported tool_choice type %q", choiceObject.Type) } if choiceObject.Function == nil || strings.TrimSpace(choiceObject.Function.Name) == "" { return toolChoiceSpec{}, fmt.Errorf("tool_choice.function.name is required") } return toolChoiceSpec{ Mode: "function", FunctionName: strings.TrimSpace(choiceObject.Function.Name), }, nil } func validateTools(tools []models.Tool) error { seen := make(map[string]struct{}, len(tools)) for _, tool := range tools { toolType := tool.Type if toolType == "" { toolType = "function" } if toolType != "function" { return fmt.Errorf("unsupported tool type %q", tool.Type) } name := strings.TrimSpace(tool.Function.Name) if name == "" { return fmt.Errorf("tool function name is required") } if _, exists := seen[name]; exists { return fmt.Errorf("duplicate tool function name %q", name) } seen[name] = struct{}{} } return nil } func toolExists(tools []models.Tool, name string) bool { for _, tool := range tools { if strings.TrimSpace(tool.Function.Name) == name { return true } } return false } func buildCursorMessages( messages []models.Message, systemPromptInject string, tools []models.Tool, toolChoice toolChoiceSpec, capability models.ModelCapability, hasToolHistory bool, triggerSignal string, ) []models.CursorMessage { result := make([]models.CursorMessage, 0, len(messages)+1) startIdx := 0 systemSegments := make([]string, 0, 3) if len(messages) > 0 && strings.EqualFold(messages[0].Role, "system") { if systemText := strings.TrimSpace(messages[0].GetStringContent()); systemText != "" { systemSegments = append(systemSegments, systemText) } startIdx = 1 } if inject := strings.TrimSpace(systemPromptInject); inject != "" { systemSegments = append(systemSegments, inject) } if protocolText := strings.TrimSpace(buildProtocolPrompt(tools, toolChoice, capability.ThinkingEnabled, hasToolHistory, triggerSignal)); protocolText != "" { systemSegments = append(systemSegments, protocolText) } if len(systemSegments) > 0 { result = append(result, newCursorTextMessage("system", strings.Join(systemSegments, "\n\n"))) } for _, msg := range messages[startIdx:] { converted, ok := convertMessage(msg, capability.ThinkingEnabled, triggerSignal) if !ok { continue } result = append(result, converted) } return result } func buildProtocolPrompt(tools []models.Tool, toolChoice toolChoiceSpec, thinkingEnabled bool, hasToolHistory bool, triggerSignal string) string { var sections []string if len(tools) > 0 && triggerSignal != "" { var builder strings.Builder builder.WriteString("You may call external tools through the bridge below.\n") builder.WriteString("When you need a tool, output exactly in this format with no markdown fences:\n") builder.WriteString(triggerSignal) builder.WriteString("\n{\"arg\":\"value\"}\n") builder.WriteString("Available tools:\n") builder.WriteString(renderFunctionList(tools)) switch toolChoice.Mode { case "required": builder.WriteString("\nYou must call at least one tool before your final answer.") builder.WriteString("\nIMPORTANT: Your next assistant message MUST be a tool call using the exact format above. Do not include any natural language text in that message.") case "function": builder.WriteString(fmt.Sprintf("\nYou must call the function %q before your final answer.", toolChoice.FunctionName)) builder.WriteString("\nIMPORTANT: Your next assistant message MUST be a tool call using the exact format above. Do not include any natural language text in that message.") } sections = append(sections, builder.String()) } else if hasToolHistory && triggerSignal != "" { var builder strings.Builder builder.WriteString("Previous assistant tool calls in this conversation are serialized in the following format:\n") builder.WriteString(triggerSignal) builder.WriteString("\n{\"arg\":\"value\"}\n") builder.WriteString("Previous tool results are serialized as ....\n") builder.WriteString("Treat those tool transcripts as completed history. Do not emit a new tool call unless a current tool list is provided.") sections = append(sections, builder.String()) } if thinkingEnabled { sections = append(sections, thinkingHint) } return strings.Join(sections, "\n\n") } func messagesContainToolHistory(messages []models.Message) bool { for _, msg := range messages { if len(msg.ToolCalls) > 0 { return true } if strings.EqualFold(strings.TrimSpace(msg.Role), "tool") { return true } } return false } func renderFunctionList(tools []models.Tool) string { var builder strings.Builder builder.WriteString("\n") for _, tool := range tools { schema := "{}" if len(tool.Function.Parameters) > 0 { if marshaled, err := json.MarshalIndent(tool.Function.Parameters, "", " "); err == nil { schema = string(marshaled) } } builder.WriteString(fmt.Sprintf("\n", tool.Function.Name)) if desc := strings.TrimSpace(tool.Function.Description); desc != "" { builder.WriteString(desc) builder.WriteString("\n") } builder.WriteString("JSON Schema:\n") builder.WriteString(schema) builder.WriteString("\n\n") } builder.WriteString("") return builder.String() } func convertMessage(msg models.Message, thinkingEnabled bool, triggerSignal string) (models.CursorMessage, bool) { role := strings.TrimSpace(msg.Role) if role == "" { return models.CursorMessage{}, false } switch role { case "tool": return newCursorTextMessage("user", formatToolResult(msg)), true case "assistant": text := strings.TrimSpace(msg.GetStringContent()) segments := make([]string, 0, len(msg.ToolCalls)+1) if text != "" { segments = append(segments, text) } for _, toolCall := range msg.ToolCalls { segments = append(segments, formatAssistantToolCall(toolCall, triggerSignal)) } if len(segments) == 0 { return models.CursorMessage{}, false } return newCursorTextMessage("assistant", strings.Join(segments, "\n\n")), true case "user": text := msg.GetStringContent() if thinkingEnabled { text = appendThinkingHint(text) } if strings.TrimSpace(text) == "" { return models.CursorMessage{}, false } return newCursorTextMessage("user", text), true default: text := msg.GetStringContent() if strings.TrimSpace(text) == "" { return models.CursorMessage{}, false } return newCursorTextMessage(role, text), true } } func appendThinkingHint(content string) string { content = strings.TrimSpace(content) if content == "" { return thinkingHint } return content + "\n\n" + thinkingHint } func formatAssistantToolCall(toolCall models.ToolCall, triggerSignal string) string { pieces := make([]string, 0, 2) if triggerSignal != "" { pieces = append(pieces, triggerSignal) } callType := toolCall.Type if callType == "" { callType = "function" } name := strings.TrimSpace(toolCall.Function.Name) if name == "" { name = "tool" } arguments := strings.TrimSpace(toolCall.Function.Arguments) if arguments == "" { arguments = "{}" } pieces = append(pieces, fmt.Sprintf("%s", name, arguments)) return strings.Join(pieces, "\n") } func formatToolResult(msg models.Message) string { content := msg.GetStringContent() id := strings.TrimSpace(msg.ToolCallID) name := strings.TrimSpace(msg.Name) var builder strings.Builder builder.WriteString("") builder.WriteString(content) builder.WriteString("") return builder.String() } func newCursorTextMessage(role, text string) models.CursorMessage { return models.CursorMessage{ Role: role, Parts: []models.CursorPart{ { Type: "text", Text: text, }, }, } }