package services import ( "bytes" "github.com/libaxuan/cursor2api-go/models" "encoding/json" "fmt" "strings" ) // ResponseToolAdapter records tool mappings for Responses API bridging. type ResponseToolAdapter struct { ToolTypesByName map[string]string ShellEnvironment interface{} } // BuildChatCompletionRequestFromResponse converts a Responses API request into a chat/completions request. 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: // Fallback: expose built-in tools as custom function tools for compatibility. 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 } }