| package services |
|
|
| import ( |
| "bytes" |
| "github.com/libaxuan/cursor2api-go/models" |
| "encoding/json" |
| "fmt" |
| "strings" |
| ) |
|
|
| |
| type ResponseToolAdapter struct { |
| ToolTypesByName map[string]string |
| ShellEnvironment interface{} |
| } |
|
|
| |
| func BuildChatCompletionRequestFromResponse(req *models.ResponseRequest) (*models.ChatCompletionRequest, *ResponseToolAdapter, error) { |
| instructionMessages, err := parseResponseMessages(req.Instructions, "system") |
| if err != nil { |
| return nil, nil, err |
| } |
|
|
| inputMessages, err := parseResponseMessages(req.Input, "user") |
| if err != nil { |
| return nil, nil, err |
| } |
|
|
| messages := append(instructionMessages, inputMessages...) |
| if len(messages) == 0 { |
| return nil, nil, fmt.Errorf("input is required") |
| } |
|
|
| tools, adapter, err := convertResponseTools(req.Tools) |
| if err != nil { |
| return nil, nil, err |
| } |
|
|
| toolChoice, err := convertResponseToolChoice(req.ToolChoice) |
| if err != nil { |
| return nil, nil, err |
| } |
|
|
| chatReq := &models.ChatCompletionRequest{ |
| Model: req.Model, |
| Messages: messages, |
| Stream: req.Stream, |
| Temperature: req.Temperature, |
| MaxTokens: req.MaxOutputTokens, |
| TopP: req.TopP, |
| Tools: tools, |
| ToolChoice: toolChoice, |
| } |
|
|
| if req.User != nil { |
| chatReq.User = *req.User |
| } |
|
|
| return chatReq, adapter, nil |
| } |
|
|
| func parseResponseMessages(raw json.RawMessage, defaultRole string) ([]models.Message, error) { |
| trimmed := bytes.TrimSpace(raw) |
| if len(trimmed) == 0 || string(trimmed) == "null" { |
| return nil, nil |
| } |
|
|
| switch trimmed[0] { |
| case '"': |
| var text string |
| if err := json.Unmarshal(trimmed, &text); err != nil { |
| return nil, fmt.Errorf("invalid input text: %w", err) |
| } |
| if strings.TrimSpace(text) == "" { |
| return nil, nil |
| } |
| return []models.Message{{Role: defaultRole, Content: text}}, nil |
| case '{': |
| var obj map[string]interface{} |
| if err := json.Unmarshal(trimmed, &obj); err != nil { |
| return nil, fmt.Errorf("invalid input object: %w", err) |
| } |
| msgs := parseResponseItem(obj, defaultRole) |
| return msgs, nil |
| case '[': |
| var arr []interface{} |
| if err := json.Unmarshal(trimmed, &arr); err != nil { |
| return nil, fmt.Errorf("invalid input array: %w", err) |
| } |
| var result []models.Message |
| for _, item := range arr { |
| switch typed := item.(type) { |
| case map[string]interface{}: |
| result = append(result, parseResponseItem(typed, defaultRole)...) |
| case string: |
| if strings.TrimSpace(typed) != "" { |
| result = append(result, models.Message{Role: defaultRole, Content: typed}) |
| } |
| } |
| } |
| return result, nil |
| default: |
| return nil, fmt.Errorf("unsupported input format") |
| } |
| } |
|
|
| func parseResponseItem(item map[string]interface{}, defaultRole string) []models.Message { |
| itemType := strings.TrimSpace(asString(item["type"])) |
| role := strings.TrimSpace(asString(item["role"])) |
|
|
| if itemType == "" && role != "" { |
| itemType = "message" |
| } |
|
|
| switch itemType { |
| case "message": |
| if role == "" { |
| role = defaultRole |
| } |
| role = mapRole(role) |
| content := extractContentText(item["content"]) |
| if strings.TrimSpace(content) == "" { |
| return nil |
| } |
| return []models.Message{{Role: role, Content: content}} |
| case "function_call": |
| callID := firstNonEmpty(asString(item["call_id"]), asString(item["id"])) |
| name := strings.TrimSpace(asString(item["name"])) |
| args := strings.TrimSpace(stringifyJSON(item["arguments"])) |
| if args == "" { |
| args = "{}" |
| } |
| toolCall := models.ToolCall{ |
| ID: callID, |
| Type: "function", |
| Function: models.FunctionCall{ |
| Name: name, |
| Arguments: args, |
| }, |
| } |
| return []models.Message{{Role: "assistant", ToolCalls: []models.ToolCall{toolCall}}} |
| case "tool_call": |
| callID := firstNonEmpty(asString(item["call_id"]), asString(item["id"])) |
| name := strings.TrimSpace(asString(item["tool_name"])) |
| if name == "" { |
| name = strings.TrimSpace(asString(item["name"])) |
| } |
| if name == "" { |
| return nil |
| } |
| args := strings.TrimSpace(stringifyJSON(item["arguments"])) |
| if args == "" { |
| if action, ok := item["action"]; ok { |
| args = wrapJSONArg("action", action) |
| } else if input := strings.TrimSpace(asString(item["input"])); input != "" { |
| args = wrapInputAsJSON(input) |
| } else { |
| args = "{}" |
| } |
| } |
| toolCall := models.ToolCall{ |
| ID: callID, |
| Type: "function", |
| Function: models.FunctionCall{ |
| Name: name, |
| Arguments: args, |
| }, |
| } |
| return []models.Message{{Role: "assistant", ToolCalls: []models.ToolCall{toolCall}}} |
| case "custom_tool_call": |
| callID := firstNonEmpty(asString(item["call_id"]), asString(item["id"])) |
| name := strings.TrimSpace(asString(item["name"])) |
| input := asString(item["input"]) |
| args := wrapInputAsJSON(input) |
| toolCall := models.ToolCall{ |
| ID: callID, |
| Type: "function", |
| Function: models.FunctionCall{ |
| Name: name, |
| Arguments: args, |
| }, |
| } |
| return []models.Message{{Role: "assistant", ToolCalls: []models.ToolCall{toolCall}}} |
| case "apply_patch_call": |
| callID := firstNonEmpty(asString(item["call_id"]), asString(item["id"])) |
| op := item["operation"] |
| args := wrapJSONArg("operation", op) |
| toolCall := models.ToolCall{ |
| ID: callID, |
| Type: "function", |
| Function: models.FunctionCall{ |
| Name: "apply_patch", |
| Arguments: args, |
| }, |
| } |
| return []models.Message{{Role: "assistant", ToolCalls: []models.ToolCall{toolCall}}} |
| case "shell_call": |
| callID := firstNonEmpty(asString(item["call_id"]), asString(item["id"])) |
| action := item["action"] |
| args := wrapJSONArg("action", action) |
| toolCall := models.ToolCall{ |
| ID: callID, |
| Type: "function", |
| Function: models.FunctionCall{ |
| Name: "shell", |
| Arguments: args, |
| }, |
| } |
| return []models.Message{{Role: "assistant", ToolCalls: []models.ToolCall{toolCall}}} |
| case "local_shell_call": |
| callID := firstNonEmpty(asString(item["call_id"]), asString(item["id"])) |
| action := item["action"] |
| args := "" |
| if action != nil { |
| args = wrapJSONArg("action", action) |
| } |
| if args == "" { |
| args = strings.TrimSpace(stringifyJSON(item["arguments"])) |
| } |
| if args == "" { |
| args = "{}" |
| } |
| toolCall := models.ToolCall{ |
| ID: callID, |
| Type: "function", |
| Function: models.FunctionCall{ |
| Name: "local_shell", |
| Arguments: args, |
| }, |
| } |
| return []models.Message{{Role: "assistant", ToolCalls: []models.ToolCall{toolCall}}} |
| case "function_call_output": |
| callID := asString(item["call_id"]) |
| output := normalizeToolOutput(item["output"]) |
| if strings.TrimSpace(output) == "" { |
| return nil |
| } |
| return []models.Message{{Role: "tool", Content: output, ToolCallID: callID}} |
| case "apply_patch_call_output": |
| callID := asString(item["call_id"]) |
| output := normalizeToolOutput(item["output"]) |
| if strings.TrimSpace(output) == "" { |
| return nil |
| } |
| return []models.Message{{Role: "tool", Content: output, ToolCallID: callID, Name: "apply_patch"}} |
| case "shell_call_output": |
| callID := asString(item["call_id"]) |
| output := normalizeToolOutput(item["output"]) |
| if strings.TrimSpace(output) == "" { |
| return nil |
| } |
| return []models.Message{{Role: "tool", Content: output, ToolCallID: callID, Name: "shell"}} |
| case "local_shell_call_output": |
| callID := asString(item["call_id"]) |
| output := normalizeToolOutput(item["output"]) |
| if strings.TrimSpace(output) == "" { |
| return nil |
| } |
| return []models.Message{{Role: "tool", Content: output, ToolCallID: callID, Name: "local_shell"}} |
| case "tool_call_output": |
| callID := asString(item["call_id"]) |
| output := normalizeToolOutput(item["output"]) |
| if strings.TrimSpace(output) == "" { |
| return nil |
| } |
| name := strings.TrimSpace(asString(item["tool_name"])) |
| if name == "" { |
| name = strings.TrimSpace(asString(item["name"])) |
| } |
| return []models.Message{{Role: "tool", Content: output, ToolCallID: callID, Name: name}} |
| default: |
| return nil |
| } |
| } |
|
|
| func convertResponseTools(rawTools []map[string]interface{}) ([]models.Tool, *ResponseToolAdapter, error) { |
| adapter := &ResponseToolAdapter{ToolTypesByName: make(map[string]string)} |
| tools := make([]models.Tool, 0, len(rawTools)) |
|
|
| for _, raw := range rawTools { |
| toolType := strings.TrimSpace(asString(raw["type"])) |
| if toolType == "" { |
| continue |
| } |
|
|
| switch toolType { |
| case "function": |
| name := strings.TrimSpace(asString(raw["name"])) |
| if name == "" { |
| return nil, nil, fmt.Errorf("function tool name is required") |
| } |
| desc := strings.TrimSpace(asString(raw["description"])) |
| params, _ := raw["parameters"].(map[string]interface{}) |
| tools = append(tools, models.Tool{ |
| Type: "function", |
| Function: models.FunctionDefinition{ |
| Name: name, |
| Description: desc, |
| Parameters: params, |
| }, |
| }) |
| case "apply_patch": |
| tools = append(tools, applyPatchToolDefinition()) |
| adapter.ToolTypesByName["apply_patch"] = "apply_patch" |
| case "shell": |
| tools = append(tools, shellToolDefinition()) |
| adapter.ToolTypesByName["shell"] = "shell" |
| if env, ok := raw["environment"]; ok { |
| adapter.ShellEnvironment = env |
| } |
| case "local_shell": |
| tools = append(tools, localShellToolDefinition()) |
| adapter.ToolTypesByName["local_shell"] = "local_shell" |
| default: |
| |
| name := toolType |
| desc := strings.TrimSpace(asString(raw["description"])) |
| if desc == "" { |
| desc = fmt.Sprintf("Built-in tool: %s (proxied).", toolType) |
| } |
| tools = append(tools, models.Tool{ |
| Type: "function", |
| Function: models.FunctionDefinition{ |
| Name: name, |
| Description: desc, |
| Parameters: map[string]interface{}{ |
| "type": "object", |
| "additionalProperties": true, |
| }, |
| }, |
| }) |
| } |
| } |
|
|
| return tools, adapter, nil |
| } |
|
|
| func applyPatchToolDefinition() models.Tool { |
| return models.Tool{ |
| Type: "function", |
| Function: models.FunctionDefinition{ |
| Name: "apply_patch", |
| Description: "Apply a patch to the local workspace. Return JSON with operation {type, path, diff}.", |
| Parameters: map[string]interface{}{ |
| "type": "object", |
| "properties": map[string]interface{}{ |
| "operation": map[string]interface{}{ |
| "type": "object", |
| "properties": map[string]interface{}{ |
| "type": map[string]interface{}{ |
| "type": "string", |
| "enum": []string{"create_file", "update_file", "delete_file"}, |
| }, |
| "path": map[string]interface{}{ |
| "type": "string", |
| }, |
| "diff": map[string]interface{}{ |
| "type": "string", |
| }, |
| }, |
| "required": []string{"type", "path"}, |
| }, |
| }, |
| "required": []string{"operation"}, |
| }, |
| }, |
| } |
| } |
|
|
| func shellToolDefinition() models.Tool { |
| return models.Tool{ |
| Type: "function", |
| Function: models.FunctionDefinition{ |
| Name: "shell", |
| Description: "Run shell commands locally. Return JSON with commands (array), timeout_ms, max_output_length.", |
| Parameters: map[string]interface{}{ |
| "type": "object", |
| "properties": map[string]interface{}{ |
| "commands": map[string]interface{}{ |
| "type": "array", |
| "items": map[string]interface{}{"type": "string"}, |
| }, |
| "timeout_ms": map[string]interface{}{ |
| "type": "integer", |
| }, |
| "max_output_length": map[string]interface{}{ |
| "type": "integer", |
| }, |
| "action": map[string]interface{}{ |
| "type": "object", |
| "properties": map[string]interface{}{ |
| "commands": map[string]interface{}{ |
| "type": "array", |
| "items": map[string]interface{}{"type": "string"}, |
| }, |
| "timeout_ms": map[string]interface{}{ |
| "type": "integer", |
| }, |
| "max_output_length": map[string]interface{}{ |
| "type": "integer", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| } |
|
|
| func localShellToolDefinition() models.Tool { |
| return models.Tool{ |
| Type: "function", |
| Function: models.FunctionDefinition{ |
| Name: "local_shell", |
| Description: "Run local shell commands. Return JSON with command, timeout_ms, working_directory, env, max_output_length.", |
| Parameters: map[string]interface{}{ |
| "type": "object", |
| "properties": map[string]interface{}{ |
| "command": map[string]interface{}{ |
| "type": "string", |
| }, |
| "commands": map[string]interface{}{ |
| "type": "array", |
| "items": map[string]interface{}{"type": "string"}, |
| }, |
| "timeout_ms": map[string]interface{}{ |
| "type": "integer", |
| }, |
| "working_directory": map[string]interface{}{ |
| "type": "string", |
| }, |
| "env": map[string]interface{}{ |
| "type": "object", |
| "additionalProperties": true, |
| }, |
| "max_output_length": map[string]interface{}{ |
| "type": "integer", |
| }, |
| "action": map[string]interface{}{ |
| "type": "object", |
| "properties": map[string]interface{}{ |
| "command": map[string]interface{}{ |
| "type": "string", |
| }, |
| "commands": map[string]interface{}{ |
| "type": "array", |
| "items": map[string]interface{}{"type": "string"}, |
| }, |
| "timeout_ms": map[string]interface{}{ |
| "type": "integer", |
| }, |
| "working_directory": map[string]interface{}{ |
| "type": "string", |
| }, |
| "env": map[string]interface{}{ |
| "type": "object", |
| "additionalProperties": true, |
| }, |
| "max_output_length": map[string]interface{}{ |
| "type": "integer", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| } |
|
|
| func convertResponseToolChoice(raw json.RawMessage) (json.RawMessage, error) { |
| trimmed := bytes.TrimSpace(raw) |
| if len(trimmed) == 0 || string(trimmed) == "null" { |
| return nil, nil |
| } |
|
|
| var choiceString string |
| if err := json.Unmarshal(trimmed, &choiceString); err == nil { |
| switch choiceString { |
| case "auto", "none", "required": |
| return trimmed, nil |
| default: |
| return nil, fmt.Errorf("unsupported tool_choice value %q", choiceString) |
| } |
| } |
|
|
| var choiceObj map[string]interface{} |
| if err := json.Unmarshal(trimmed, &choiceObj); err != nil { |
| return nil, fmt.Errorf("tool_choice must be a string or object") |
| } |
|
|
| choiceType := strings.TrimSpace(asString(choiceObj["type"])) |
| switch choiceType { |
| case "function", "custom": |
| name := strings.TrimSpace(asString(choiceObj["name"])) |
| if name == "" { |
| return nil, fmt.Errorf("tool_choice.name is required") |
| } |
| return json.Marshal(models.ToolChoiceObject{ |
| Type: "function", |
| Function: &models.ToolChoiceFunction{ |
| Name: name, |
| }, |
| }) |
| case "apply_patch": |
| return json.Marshal(models.ToolChoiceObject{ |
| Type: "function", |
| Function: &models.ToolChoiceFunction{ |
| Name: "apply_patch", |
| }, |
| }) |
| case "shell": |
| return json.Marshal(models.ToolChoiceObject{ |
| Type: "function", |
| Function: &models.ToolChoiceFunction{ |
| Name: "shell", |
| }, |
| }) |
| case "local_shell": |
| return json.Marshal(models.ToolChoiceObject{ |
| Type: "function", |
| Function: &models.ToolChoiceFunction{ |
| Name: "local_shell", |
| }, |
| }) |
| default: |
| return nil, fmt.Errorf("unsupported tool_choice type %q", choiceType) |
| } |
| } |
|
|
| func extractContentText(content interface{}) string { |
| switch v := content.(type) { |
| case string: |
| return v |
| case []interface{}: |
| var b strings.Builder |
| for _, part := range v { |
| if partMap, ok := part.(map[string]interface{}); ok { |
| partType := strings.TrimSpace(asString(partMap["type"])) |
| switch partType { |
| case "input_text", "output_text", "text": |
| b.WriteString(asString(partMap["text"])) |
| case "refusal": |
| b.WriteString(asString(partMap["refusal"])) |
| } |
| } |
| } |
| return b.String() |
| case map[string]interface{}: |
| partType := strings.TrimSpace(asString(v["type"])) |
| switch partType { |
| case "input_text", "output_text", "text": |
| return asString(v["text"]) |
| case "refusal": |
| return asString(v["refusal"]) |
| } |
| } |
|
|
| if data, err := json.Marshal(content); err == nil { |
| return string(data) |
| } |
| return "" |
| } |
|
|
| func normalizeToolOutput(output interface{}) string { |
| switch v := output.(type) { |
| case string: |
| return v |
| case []interface{}: |
| var b strings.Builder |
| for _, part := range v { |
| if partMap, ok := part.(map[string]interface{}); ok { |
| partType := strings.TrimSpace(asString(partMap["type"])) |
| switch partType { |
| case "output_text", "input_text", "text": |
| b.WriteString(asString(partMap["text"])) |
| case "refusal": |
| b.WriteString(asString(partMap["refusal"])) |
| default: |
| if data, err := json.Marshal(partMap); err == nil { |
| b.WriteString(string(data)) |
| } |
| } |
| } |
| } |
| if b.Len() > 0 { |
| return b.String() |
| } |
| case map[string]interface{}: |
| if data, err := json.Marshal(v); err == nil { |
| return string(data) |
| } |
| } |
|
|
| if data, err := json.Marshal(output); err == nil { |
| return string(data) |
| } |
| return "" |
| } |
|
|
| func wrapInputAsJSON(input string) string { |
| payload := map[string]interface{}{ |
| "input": input, |
| } |
| if data, err := json.Marshal(payload); err == nil { |
| return string(data) |
| } |
| return "{}" |
| } |
|
|
| func wrapJSONArg(key string, value interface{}) string { |
| payload := map[string]interface{}{ |
| key: value, |
| } |
| if data, err := json.Marshal(payload); err == nil { |
| return string(data) |
| } |
| return "{}" |
| } |
|
|
| func stringifyJSON(value interface{}) string { |
| switch v := value.(type) { |
| case string: |
| return v |
| case json.RawMessage: |
| return string(v) |
| default: |
| if data, err := json.Marshal(v); err == nil { |
| return string(data) |
| } |
| } |
| return "" |
| } |
|
|
| func asString(value interface{}) string { |
| switch v := value.(type) { |
| case string: |
| return v |
| case json.Number: |
| return v.String() |
| } |
| return "" |
| } |
|
|
| func firstNonEmpty(values ...string) string { |
| for _, v := range values { |
| if strings.TrimSpace(v) != "" { |
| return v |
| } |
| } |
| return "" |
| } |
|
|
| func mapRole(role string) string { |
| role = strings.ToLower(strings.TrimSpace(role)) |
| switch role { |
| case "developer": |
| return "system" |
| default: |
| if role == "" { |
| return "user" |
| } |
| return role |
| } |
| } |
|
|