package interactions import ( "fmt" "strings" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) func ConvertInteractionsRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte { root := gjson.ParseBytes(inputRawJSON) out := []byte(`{"model":"","max_tokens":32000,"messages":[]}`) out, _ = sjson.SetBytes(out, "model", modelName) if stream || root.Get("stream").Bool() { out, _ = sjson.SetBytes(out, "stream", true) } out = copyInteractionsSystemToClaude(out, root) out = copyInteractionsGenerationConfigToClaude(out, root) messageAccumulator := translatorcommon.NewClaudeMessageAccumulator(int(root.Get("input.#").Int())) appendInteractionsInputToClaudeMessages(messageAccumulator, root.Get("input")) out = translatorcommon.SetRawArrayItems(out, "messages", messageAccumulator.Messages()) out = copyInteractionsToolsToClaude(out, root) return out } func copyInteractionsSystemToClaude(out []byte, root gjson.Result) []byte { sys := root.Get("system_instruction") if !sys.Exists() { sys = root.Get("systemInstruction") } text := interactionsClaudeText(sys) if text == "" { return out } out, _ = sjson.SetBytes(out, "system", text) return out } func copyInteractionsGenerationConfigToClaude(out []byte, root gjson.Result) []byte { cfg := root.Get("generation_config") if !cfg.Exists() { cfg = root.Get("generationConfig") } if cfg.Exists() { out = copyJSONField(out, cfg, "max_output_tokens", "max_tokens") out = copyJSONField(out, cfg, "maxOutputTokens", "max_tokens") out = copyJSONField(out, cfg, "top_p", "top_p") out = copyJSONField(out, cfg, "topP", "top_p") out = copyJSONField(out, cfg, "temperature", "temperature") out = copyJSONField(out, cfg, "stop_sequences", "stop_sequences") out = copyJSONField(out, cfg, "stopSequences", "stop_sequences") out = copyInteractionsThinkingConfigToClaude(out, cfg) out = copyInteractionsToolChoiceToClaude(out, cfg.Get("tool_choice")) out = copyInteractionsToolChoiceToClaude(out, cfg.Get("toolChoice")) } out = copyInteractionsReasoningToClaude(out, root.Get("reasoning")) out = copyInteractionsToolChoiceToClaude(out, root.Get("tool_choice")) out = copyInteractionsToolChoiceToClaude(out, root.Get("toolChoice")) return out } func copyJSONField(out []byte, root gjson.Result, from, to string) []byte { value := root.Get(from) if !value.Exists() { return out } out, _ = sjson.SetRawBytes(out, to, []byte(value.Raw)) return out } func copyInteractionsThinkingConfigToClaude(out []byte, cfg gjson.Result) []byte { level := firstClaudeInteractionsExisting(cfg, "thinking_level", "thinkingLevel", "reasoning.effort") if !level.Exists() { return out } return setClaudeThinkingFromLevel(out, level.String()) } func copyInteractionsReasoningToClaude(out []byte, reasoning gjson.Result) []byte { if !reasoning.Exists() { return out } if effort := reasoning.Get("effort"); effort.Exists() { return setClaudeThinkingFromLevel(out, effort.String()) } if level := reasoning.Get("thinking_level"); level.Exists() { return setClaudeThinkingFromLevel(out, level.String()) } return out } func setClaudeThinkingFromLevel(out []byte, level string) []byte { normalized := strings.ToLower(strings.TrimSpace(level)) if normalized == "" { return out } switch normalized { case "none", "disabled", "off", "false": out, _ = sjson.SetBytes(out, "thinking.type", "disabled") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") return out case "auto", "adaptive": out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") return out } if budget, ok := thinking.ConvertLevelToBudget(normalized); ok { switch { case budget == 0: out, _ = sjson.SetBytes(out, "thinking.type", "disabled") case budget < 0: out, _ = sjson.SetBytes(out, "thinking.type", "enabled") default: out, _ = sjson.SetBytes(out, "thinking.type", "enabled") out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } return out } out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.SetBytes(out, "output_config.effort", normalized) return out } func appendInteractionsInputToClaudeMessages(accumulator *translatorcommon.ClaudeMessageAccumulator, input gjson.Result) { if !input.Exists() { return } if input.Type == gjson.String { step := []byte(`{"type":"user_input","content":[{"type":"text","text":""}]}`) step, _ = sjson.SetBytes(step, "content.0.text", input.String()) appendInteractionsStepToClaude(accumulator, gjson.ParseBytes(step), "user") return } if input.IsObject() { appendInteractionsInputItemToClaude(accumulator, input) return } input.ForEach(func(_, step gjson.Result) bool { appendInteractionsInputItemToClaude(accumulator, step) return true }) } func appendInteractionsInputItemToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result) { if step.Get("steps").IsArray() { defaultRole := "user" if role := step.Get("role").String(); role == "model" || role == "assistant" { defaultRole = "assistant" } step.Get("steps").ForEach(func(_, nestedStep gjson.Result) bool { appendInteractionsStepToClaude(accumulator, nestedStep, defaultRole) return true }) return } if step.Get("parts").Exists() { wrapped := []byte(`{"type":"user_input","content":[]}`) if role := step.Get("role").String(); role == "model" || role == "assistant" { wrapped, _ = sjson.SetBytes(wrapped, "type", "model_output") } wrapped, _ = sjson.SetRawBytes(wrapped, "content", []byte(step.Get("parts").Raw)) appendInteractionsStepToClaude(accumulator, gjson.ParseBytes(wrapped), "user") return } stepType := step.Get("type").String() switch stepType { case "function_call": appendInteractionsFunctionCallToClaude(accumulator, step) case "function_result": appendInteractionsFunctionResultToClaude(accumulator, step) case "model_output", "thought": appendInteractionsStepToClaude(accumulator, step, "assistant") default: appendInteractionsStepToClaude(accumulator, step, "user") } } func appendInteractionsStepToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result, defaultRole string) { role := defaultRole if stepRole := step.Get("role").String(); stepRole == "user" || stepRole == "assistant" { role = stepRole } contentItems := make([][]byte, 0, 4) stepContent := step.Get("content") if stepContent.Type == gjson.String { part := []byte(`{"type":"text","text":""}`) part, _ = sjson.SetBytes(part, "text", stepContent.String()) contentItems = append(contentItems, part) } else if stepContent.IsArray() { stepContent.ForEach(func(_, part gjson.Result) bool { if converted := interactionsContentToClaude(part, role); len(converted) > 0 { contentItems = append(contentItems, converted) } return true }) } else if text := step.Get("text"); text.Exists() { part := []byte(`{"type":"text","text":""}`) part, _ = sjson.SetBytes(part, "text", text.String()) contentItems = append(contentItems, part) } if len(contentItems) == 0 { return } msg := []byte(`{"role":"","content":[]}`) msg, _ = sjson.SetBytes(msg, "role", role) msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray(contentItems)) accumulator.Append(msg) } func interactionsContentToClaude(part gjson.Result, role string) []byte { partType := part.Get("type").String() if partType == "" && part.Get("text").Exists() { partType = "text" } switch partType { case "text": textPart := []byte(`{"type":"text","text":""}`) textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String()) return textPart case "thinking", "reasoning": if role != "assistant" { return nil } thinkingPart := []byte(`{"type":"thinking","thinking":""}`) thinkingPart, _ = sjson.SetBytes(thinkingPart, "thinking", interactionsClaudeText(part)) return thinkingPart case "image": imagePart, _ := interactionsClaudeMediaPart(part, "image") return imagePart case "document", "file": documentPart, _ := interactionsClaudeMediaPart(part, "document") return documentPart default: if text := interactionsClaudeText(part); text != "" { textPart := []byte(`{"type":"text","text":""}`) textPart, _ = sjson.SetBytes(textPart, "text", text) return textPart } if part.Get("data").String() != "" || part.Get("file_data").String() != "" { textPart := []byte(`{"type":"text","text":""}`) textPart, _ = sjson.SetBytes(textPart, "text", fmt.Sprintf("[%s content omitted]", partType)) return textPart } } return nil } func appendInteractionsFunctionCallToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result) { toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) toolUse, _ = sjson.SetBytes(toolUse, "id", interactionsClaudeToolID(step)) toolUse, _ = sjson.SetBytes(toolUse, "name", step.Get("name").String()) args := step.Get("arguments") if !args.Exists() { args = step.Get("args") } if args.Exists() && args.IsObject() { toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(args.Raw)) } msg := []byte(`{"role":"assistant","content":[]}`) msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray([][]byte{toolUse})) accumulator.Append(msg) } func appendInteractionsFunctionResultToClaude(accumulator *translatorcommon.ClaudeMessageAccumulator, step gjson.Result) { toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`) toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", interactionsClaudeToolID(step)) result := step.Get("result") if !result.Exists() { result = step.Get("output") } switch { case result.IsArray(): contentItems := make([][]byte, 0, 4) result.ForEach(func(_, part gjson.Result) bool { if converted := interactionsContentToClaude(part, "user"); len(converted) > 0 { contentItems = append(contentItems, converted) } return true }) toolResult, _ = sjson.SetRawBytes(toolResult, "content", translatorcommon.JoinRawArray(contentItems)) case result.Exists() && result.Raw != "": toolResult, _ = sjson.SetBytes(toolResult, "content", result.Raw) default: toolResult, _ = sjson.SetBytes(toolResult, "content", "") } msg := []byte(`{"role":"user","content":[]}`) msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray([][]byte{toolResult})) accumulator.Append(msg) } func copyInteractionsToolsToClaude(out []byte, root gjson.Result) []byte { tools := root.Get("tools") if !tools.Exists() || !tools.IsArray() { return out } var toolItems [][]byte tools.ForEach(func(_, tool gjson.Result) bool { if tool.Get("function_declarations").IsArray() { tool.Get("function_declarations").ForEach(func(_, decl gjson.Result) bool { if converted := interactionsClaudeTool(decl); len(converted) > 0 { toolItems = append(toolItems, converted) } return true }) return true } if tool.Get("functionDeclarations").IsArray() { tool.Get("functionDeclarations").ForEach(func(_, decl gjson.Result) bool { if converted := interactionsClaudeTool(decl); len(converted) > 0 { toolItems = append(toolItems, converted) } return true }) return true } if converted := interactionsClaudeTool(tool); len(converted) > 0 { toolItems = append(toolItems, converted) } return true }) if len(toolItems) > 0 { out, _ = sjson.SetRawBytes(out, "tools", translatorcommon.JoinRawArray(toolItems)) } return out } func interactionsClaudeTool(tool gjson.Result) []byte { name := tool.Get("name").String() if name == "" { name = tool.Get("function.name").String() } if name == "" { return nil } converted := []byte(`{"name":"","input_schema":{}}`) converted, _ = sjson.SetBytes(converted, "name", name) if desc := tool.Get("description"); desc.Exists() { converted, _ = sjson.SetBytes(converted, "description", desc.String()) } else if desc := tool.Get("function.description"); desc.Exists() { converted, _ = sjson.SetBytes(converted, "description", desc.String()) } params := firstClaudeInteractionsExisting(tool, "parameters", "parametersJsonSchema", "parameters_json_schema", "input_schema") if params.Exists() && params.IsObject() { converted, _ = sjson.SetRawBytes(converted, "input_schema", []byte(params.Raw)) } return converted } func copyInteractionsToolChoiceToClaude(out []byte, toolChoice gjson.Result) []byte { if !toolChoice.Exists() { return out } switch toolChoice.Type { case gjson.String: switch strings.ToLower(strings.TrimSpace(toolChoice.String())) { case "auto": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "required", "any": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) } case gjson.JSON: toolType := strings.ToLower(strings.TrimSpace(toolChoice.Get("type").String())) switch toolType { case "auto": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "required", "any": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) case "function", "tool": name := toolChoice.Get("name").String() if name == "" { name = toolChoice.Get("function.name").String() } if name != "" { choice := []byte(`{"type":"tool","name":""}`) choice, _ = sjson.SetBytes(choice, "name", name) out, _ = sjson.SetRawBytes(out, "tool_choice", choice) } } } return out } func interactionsClaudeToolID(step gjson.Result) string { for _, path := range []string{"call_id", "id", "tool_use_id"} { if value := step.Get(path).String(); value != "" { return util.SanitizeClaudeToolID(value) } } if name := step.Get("name").String(); name != "" { return util.SanitizeClaudeToolID("toolu_" + name) } return "toolu_interactions" } func interactionsClaudeText(value gjson.Result) string { if !value.Exists() { return "" } if value.Type == gjson.String { return value.String() } if text := value.Get("text"); text.Exists() { return text.String() } if thinking := value.Get("thinking"); thinking.Exists() { return thinking.String() } if content := value.Get("content"); content.Exists() { return interactionsClaudeText(content) } if parts := value.Get("parts"); parts.Exists() && parts.IsArray() { var builder strings.Builder parts.ForEach(func(_, part gjson.Result) bool { text := interactionsClaudeText(part) if text == "" { return true } if builder.Len() > 0 { builder.WriteByte('\n') } builder.WriteString(text) return true }) return builder.String() } return "" } func interactionsClaudeMediaPart(part gjson.Result, claudeType string) ([]byte, bool) { mimeType := firstClaudeInteractionsExisting(part, "mime_type", "mimeType", "media_type", "mediaType").String() data := firstClaudeInteractionsExisting(part, "data", "file_data", "fileData").String() if source := part.Get("source"); source.Exists() { if mimeType == "" { mimeType = source.Get("media_type").String() } if data == "" { data = source.Get("data").String() } } if mimeType == "" || data == "" { return nil, false } out := []byte(`{"type":"","source":{"type":"base64","media_type":"","data":""}}`) out, _ = sjson.SetBytes(out, "type", claudeType) out, _ = sjson.SetBytes(out, "source.media_type", mimeType) out, _ = sjson.SetBytes(out, "source.data", data) return out, true } func firstClaudeInteractionsExisting(root gjson.Result, paths ...string) gjson.Result { for _, path := range paths { if value := root.Get(path); value.Exists() { return value } } return gjson.Result{} }