cursor / services /response_adapter.go
cacode's picture
Upload 48 files
1766992 verified
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
}
}