package responses import ( "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "math/big" "strings" "github.com/google/uuid" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) var ( user = "" account = "" session = "" ) // ConvertOpenAIResponsesRequestToClaude transforms an OpenAI Responses API request // into a Claude Messages API request using only gjson/sjson for JSON handling. // It supports: // - instructions -> system message // - input[].type==message with input_text/output_text -> user/assistant messages // - function_call -> assistant tool_use // - function_call_output -> user tool_result // - tools[].parameters -> tools[].input_schema // - max_output_tokens -> max_tokens // - stream passthrough via parameter func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := inputRawJSON if account == "" { u, _ := uuid.NewRandom() account = u.String() } if session == "" { u, _ := uuid.NewRandom() session = u.String() } if user == "" { sum := sha256.Sum256([]byte(account + session)) user = hex.EncodeToString(sum[:]) } userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session) // Base Claude message payload out := []byte(fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID)) root := gjson.ParseBytes(rawJSON) // Convert OpenAI Responses reasoning.effort to Claude thinking config. if v := root.Get("reasoning.effort"); v.Exists() { effort := strings.ToLower(strings.TrimSpace(v.String())) if effort != "" { mi := registry.LookupModelInfo(modelName, "claude") supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) // Claude 4.6 supports adaptive thinking with output_config.effort. // MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid // validation errors since validate treats same-provider unsupported levels as errors. if supportsAdaptive { switch effort { case "none": out, _ = sjson.SetBytes(out, "thinking.type", "disabled") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.DeleteBytes(out, "output_config.effort") case "auto": out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.DeleteBytes(out, "output_config.effort") default: if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { effort = mapped } out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.SetBytes(out, "output_config.effort", effort) } } else { // Legacy/manual thinking (budget_tokens). budget, ok := thinking.ConvertLevelToBudget(effort) if ok { switch budget { case 0: out, _ = sjson.SetBytes(out, "thinking.type", "disabled") case -1: out, _ = sjson.SetBytes(out, "thinking.type", "enabled") default: if budget > 0 { out, _ = sjson.SetBytes(out, "thinking.type", "enabled") out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } } } } // Helper for generating tool call IDs when missing genToolCallID := func() string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var b strings.Builder for i := 0; i < 24; i++ { n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) b.WriteByte(letters[n.Int64()]) } return "toolu_" + b.String() } // Model out, _ = sjson.SetBytes(out, "model", modelName) // Max tokens if mot := root.Get("max_output_tokens"); mot.Exists() { out, _ = sjson.SetBytes(out, "max_tokens", mot.Int()) } // Stream out, _ = sjson.SetBytes(out, "stream", stream) // instructions -> as a leading message (use role user for Claude API compatibility) instructionsText := "" extractedFromSystem := false if instr := root.Get("instructions"); instr.Exists() && instr.Type == gjson.String { instructionsText = instr.String() if instructionsText != "" { sysMsg := []byte(`{"role":"user","content":""}`) sysMsg, _ = sjson.SetBytes(sysMsg, "content", instructionsText) out, _ = sjson.SetRawBytes(out, "messages.-1", sysMsg) } } if instructionsText == "" { if input := root.Get("input"); input.Exists() && input.IsArray() { input.ForEach(func(_, item gjson.Result) bool { if strings.EqualFold(item.Get("role").String(), "system") { var builder strings.Builder if parts := item.Get("content"); parts.Exists() && parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { textResult := part.Get("text") text := textResult.String() if builder.Len() > 0 && text != "" { builder.WriteByte('\n') } builder.WriteString(text) return true }) } else if parts.Type == gjson.String { builder.WriteString(parts.String()) } instructionsText = builder.String() if instructionsText != "" { sysMsg := []byte(`{"role":"user","content":""}`) sysMsg, _ = sjson.SetBytes(sysMsg, "content", instructionsText) out, _ = sjson.SetRawBytes(out, "messages.-1", sysMsg) extractedFromSystem = true } } return instructionsText == "" }) } } // input array processing if input := root.Get("input"); input.Exists() && input.IsArray() { input.ForEach(func(_, item gjson.Result) bool { if extractedFromSystem && strings.EqualFold(item.Get("role").String(), "system") { return true } typ := item.Get("type").String() if typ == "" && item.Get("role").String() != "" { typ = "message" } switch typ { case "message": // Determine role and construct Claude-compatible content parts. var role string var textAggregate strings.Builder var partsJSON []string hasImage := false hasFile := false if parts := item.Get("content"); parts.Exists() && parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { ptype := part.Get("type").String() switch ptype { case "input_text", "output_text": if t := part.Get("text"); t.Exists() { txt := t.String() textAggregate.WriteString(txt) contentPart := []byte(`{"type":"text","text":""}`) contentPart, _ = sjson.SetBytes(contentPart, "text", txt) partsJSON = append(partsJSON, string(contentPart)) } if ptype == "input_text" { role = "user" } else { role = "assistant" } case "input_image": url := part.Get("image_url").String() if url == "" { url = part.Get("url").String() } if url != "" { var contentPart []byte if strings.HasPrefix(url, "data:") { trimmed := strings.TrimPrefix(url, "data:") mediaAndData := strings.SplitN(trimmed, ";base64,", 2) mediaType := "application/octet-stream" data := "" if len(mediaAndData) == 2 { if mediaAndData[0] != "" { mediaType = mediaAndData[0] } data = mediaAndData[1] } if data != "" { contentPart = []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`) contentPart, _ = sjson.SetBytes(contentPart, "source.media_type", mediaType) contentPart, _ = sjson.SetBytes(contentPart, "source.data", data) } } else { contentPart = []byte(`{"type":"image","source":{"type":"url","url":""}}`) contentPart, _ = sjson.SetBytes(contentPart, "source.url", url) } if len(contentPart) > 0 { partsJSON = append(partsJSON, string(contentPart)) if role == "" { role = "user" } hasImage = true } } case "input_file": fileData := part.Get("file_data").String() if fileData != "" { mediaType := "application/octet-stream" data := fileData if strings.HasPrefix(fileData, "data:") { trimmed := strings.TrimPrefix(fileData, "data:") mediaAndData := strings.SplitN(trimmed, ";base64,", 2) if len(mediaAndData) == 2 { if mediaAndData[0] != "" { mediaType = mediaAndData[0] } data = mediaAndData[1] } } contentPart := []byte(`{"type":"document","source":{"type":"base64","media_type":"","data":""}}`) contentPart, _ = sjson.SetBytes(contentPart, "source.media_type", mediaType) contentPart, _ = sjson.SetBytes(contentPart, "source.data", data) partsJSON = append(partsJSON, string(contentPart)) if role == "" { role = "user" } hasFile = true } } return true }) } else if parts.Type == gjson.String { textAggregate.WriteString(parts.String()) } // Fallback to given role if content types not decisive if role == "" { r := item.Get("role").String() switch r { case "user", "assistant", "system": role = r default: role = "user" } } if len(partsJSON) > 0 { msg := []byte(`{"role":"","content":[]}`) msg, _ = sjson.SetBytes(msg, "role", role) if len(partsJSON) == 1 && !hasImage && !hasFile { // Preserve legacy behavior for single text content msg, _ = sjson.DeleteBytes(msg, "content") textPart := gjson.Parse(partsJSON[0]) msg, _ = sjson.SetBytes(msg, "content", textPart.Get("text").String()) } else { for _, partJSON := range partsJSON { msg, _ = sjson.SetRawBytes(msg, "content.-1", []byte(partJSON)) } } out, _ = sjson.SetRawBytes(out, "messages.-1", msg) } else if textAggregate.Len() > 0 || role == "system" { msg := []byte(`{"role":"","content":""}`) msg, _ = sjson.SetBytes(msg, "role", role) msg, _ = sjson.SetBytes(msg, "content", textAggregate.String()) out, _ = sjson.SetRawBytes(out, "messages.-1", msg) } case "function_call": // Map to assistant tool_use callID := item.Get("call_id").String() if callID == "" { callID = genToolCallID() } name := item.Get("name").String() argsStr := item.Get("arguments").String() toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) toolUse, _ = sjson.SetBytes(toolUse, "id", callID) toolUse, _ = sjson.SetBytes(toolUse, "name", name) if argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) if argsJSON.IsObject() { toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw)) } } asst := []byte(`{"role":"assistant","content":[]}`) asst, _ = sjson.SetRawBytes(asst, "content.-1", toolUse) out, _ = sjson.SetRawBytes(out, "messages.-1", asst) case "function_call_output": // Map to user tool_result callID := item.Get("call_id").String() outputStr := item.Get("output").String() toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`) toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", callID) toolResult, _ = sjson.SetBytes(toolResult, "content", outputStr) usr := []byte(`{"role":"user","content":[]}`) usr, _ = sjson.SetRawBytes(usr, "content.-1", toolResult) out, _ = sjson.SetRawBytes(out, "messages.-1", usr) } return true }) } includedToolNames := map[string]struct{}{} toolNameMap := map[string]string{} // tools mapping: parameters -> input_schema if tools := root.Get("tools"); tools.Exists() && tools.IsArray() { toolsJSON := []byte("[]") tools.ForEach(func(_, tool gjson.Result) bool { convertedTools := convertResponsesToolToClaudeTools(tool, toolNameMap) for _, tJSON := range convertedTools { toolName := gjson.GetBytes(tJSON, "name").String() if toolName != "" { includedToolNames[toolName] = struct{}{} } toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "-1", tJSON) } return true }) if parsedTools := gjson.ParseBytes(toolsJSON); parsedTools.IsArray() && len(parsedTools.Array()) > 0 { out, _ = sjson.SetRawBytes(out, "tools", toolsJSON) } } // Map tool_choice similar to Chat Completions translator (optional in docs, safe to handle) if toolChoice := root.Get("tool_choice"); toolChoice.Exists() { switch toolChoice.Type { case gjson.String: switch toolChoice.String() { case "auto": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "none": // Leave unset; implies no tools case "required": if len(includedToolNames) > 0 { out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) } } case gjson.JSON: if toolChoice.Get("type").String() == "function" { fn := toolChoice.Get("function.name").String() if fn == "" { fn = toolChoice.Get("name").String() } if mappedName := toolNameMap[fn]; mappedName != "" { fn = mappedName } if _, ok := includedToolNames[fn]; ok { toolChoiceJSON := []byte(`{"name":"","type":"tool"}`) toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "name", fn) out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON) } } default: } } return out } func convertResponsesToolToClaudeTools(tool gjson.Result, toolNameMap map[string]string) [][]byte { toolType := strings.TrimSpace(tool.Get("type").String()) switch toolType { case "", "function": if tJSON, ok := convertResponsesFunctionToolToClaude(tool, ""); ok { return [][]byte{tJSON} } case "namespace": return convertResponsesNamespaceToolToClaude(tool, toolNameMap) case "web_search": if tJSON, ok := convertResponsesWebSearchToolToClaude(tool); ok { if name := gjson.GetBytes(tJSON, "name").String(); name != "" { toolNameMap[name] = name } return [][]byte{tJSON} } default: if isUnsupportedOpenAIBuiltinToolType(toolType) { return nil } if tool.Get("name").String() != "" { return [][]byte{[]byte(tool.Raw)} } } return nil } func convertResponsesNamespaceToolToClaude(tool gjson.Result, toolNameMap map[string]string) [][]byte { namespaceName := strings.TrimSpace(tool.Get("name").String()) children := tool.Get("tools") if !children.Exists() || !children.IsArray() { return nil } var out [][]byte children.ForEach(func(_, child gjson.Result) bool { childName := responsesToolName(child) qualifiedName := qualifyResponsesNamespaceToolName(namespaceName, childName) if tJSON, ok := convertResponsesFunctionToolToClaude(child, qualifiedName); ok { out = append(out, tJSON) toolNameMap[qualifiedName] = qualifiedName if childName != "" { toolNameMap[childName] = qualifiedName } } return true }) return out } func convertResponsesFunctionToolToClaude(tool gjson.Result, overrideName string) ([]byte, bool) { name := strings.TrimSpace(overrideName) if name == "" { name = responsesToolName(tool) } if name == "" { return nil, false } tJSON := []byte(`{"name":"","description":"","input_schema":{}}`) tJSON, _ = sjson.SetBytes(tJSON, "name", name) if d := responsesToolDescription(tool); d != "" { tJSON, _ = sjson.SetBytes(tJSON, "description", d) } tJSON, _ = sjson.SetRawBytes(tJSON, "input_schema", normalizeClaudeToolInputSchema(responsesToolParameters(tool))) return tJSON, true } func convertResponsesWebSearchToolToClaude(tool gjson.Result) ([]byte, bool) { if externalWebAccess := tool.Get("external_web_access"); externalWebAccess.Exists() && !externalWebAccess.Bool() { return nil, false } name := strings.TrimSpace(tool.Get("name").String()) if name == "" { name = "web_search" } tJSON := []byte(`{"type":"web_search_20250305","name":""}`) tJSON, _ = sjson.SetBytes(tJSON, "name", name) if maxUses := tool.Get("max_uses"); maxUses.Exists() { tJSON, _ = sjson.SetBytes(tJSON, "max_uses", maxUses.Int()) } if allowedDomains := tool.Get("filters.allowed_domains"); allowedDomains.Exists() && allowedDomains.IsArray() { tJSON, _ = sjson.SetRawBytes(tJSON, "allowed_domains", []byte(allowedDomains.Raw)) } if userLocation := tool.Get("user_location"); userLocation.Exists() && userLocation.IsObject() { tJSON, _ = sjson.SetRawBytes(tJSON, "user_location", []byte(userLocation.Raw)) } return tJSON, true } func responsesToolName(tool gjson.Result) string { if name := strings.TrimSpace(tool.Get("name").String()); name != "" { return name } return strings.TrimSpace(tool.Get("function.name").String()) } func responsesToolDescription(tool gjson.Result) string { if description := tool.Get("description").String(); description != "" { return description } return tool.Get("function.description").String() } func responsesToolParameters(tool gjson.Result) gjson.Result { for _, path := range []string{ "parameters", "parametersJsonSchema", "input_schema", "function.parameters", "function.parametersJsonSchema", } { if parameters := tool.Get(path); parameters.Exists() { return parameters } } return gjson.Result{} } func normalizeClaudeToolInputSchema(parameters gjson.Result) []byte { raw := strings.TrimSpace(parameters.Raw) if raw == "" || raw == "null" || !gjson.Valid(raw) { return []byte(`{"type":"object","properties":{}}`) } result := gjson.Parse(raw) if !result.IsObject() { return []byte(`{"type":"object","properties":{}}`) } schema := []byte(raw) schemaType := result.Get("type").String() if schemaType == "" { schema, _ = sjson.SetBytes(schema, "type", "object") schemaType = "object" } if schemaType == "object" && !result.Get("properties").Exists() { schema, _ = sjson.SetRawBytes(schema, "properties", []byte(`{}`)) } return schema } func qualifyResponsesNamespaceToolName(namespaceName, childName string) string { childName = strings.TrimSpace(childName) if childName == "" || namespaceName == "" || strings.HasPrefix(childName, "mcp__") { return childName } if strings.HasPrefix(childName, namespaceName) { return childName } if strings.HasSuffix(namespaceName, "__") { return namespaceName + childName } return namespaceName + "__" + childName } func isUnsupportedOpenAIBuiltinToolType(toolType string) bool { switch toolType { case "image_generation", "file_search", "code_interpreter", "computer_use_preview": return true default: return false } }