package interactions import ( "encoding/json" "fmt" "strings" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) func ConvertInteractionsRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte { root := gjson.ParseBytes(inputRawJSON) out := []byte(`{"model":"","instructions":"","input":[]}`) out, _ = sjson.SetBytes(out, "model", modelName) if stream || root.Get("stream").Bool() { out, _ = sjson.SetBytes(out, "stream", true) } out = copyInteractionsSystemToCodex(out, root) out = copyInteractionsGenerationConfigToCodex(out, root) out = appendInteractionsInputToCodex(out, root.Get("input")) out = copyInteractionsToolsToCodex(out, root) out = copyInteractionsCodexTopLevel(out, root) return out } func copyInteractionsSystemToCodex(out []byte, root gjson.Result) []byte { systemInstruction := root.Get("system_instruction") if !systemInstruction.Exists() { systemInstruction = root.Get("systemInstruction") } if !systemInstruction.Exists() { return out } if systemInstruction.Type == gjson.String { out, _ = sjson.SetBytes(out, "instructions", systemInstruction.String()) return out } if text := systemInstruction.Get("text"); text.Exists() && text.Type == gjson.String { out, _ = sjson.SetBytes(out, "instructions", text.String()) return out } if parts := systemInstruction.Get("parts"); parts.Exists() && parts.IsArray() { var builder strings.Builder parts.ForEach(func(_, part gjson.Result) bool { text := part.Get("text").String() if text == "" { return true } if builder.Len() > 0 { builder.WriteByte('\n') } builder.WriteString(text) return true }) if builder.Len() > 0 { out, _ = sjson.SetBytes(out, "instructions", builder.String()) } } return out } func copyInteractionsGenerationConfigToCodex(out []byte, root gjson.Result) []byte { cfg := root.Get("generation_config") if !cfg.Exists() { cfg = root.Get("generationConfig") } if !cfg.Exists() { if reasoning := root.Get("reasoning"); reasoning.Exists() { out, _ = sjson.SetRawBytes(out, "reasoning", []byte(reasoning.Raw)) } return out } if reasoning := cfg.Get("reasoning"); reasoning.Exists() { out, _ = sjson.SetRawBytes(out, "reasoning", []byte(reasoning.Raw)) } if effort := interactionsCodexReasoningEffort(cfg); effort != "" { out, _ = sjson.SetBytes(out, "reasoning.effort", effort) } if summary := interactionsCodexReasoningSummary(cfg); summary != "" { out, _ = sjson.SetBytes(out, "reasoning.summary", summary) } copyRawPaths := map[string]string{ "max_output_tokens": "max_output_tokens", "maxOutputTokens": "max_output_tokens", "max_tokens": "max_output_tokens", "temperature": "temperature", "top_p": "top_p", "topP": "top_p", "presence_penalty": "presence_penalty", "presencePenalty": "presence_penalty", "frequency_penalty": "frequency_penalty", "frequencyPenalty": "frequency_penalty", "parallel_tool_calls": "parallel_tool_calls", "parallelToolCalls": "parallel_tool_calls", "response_format": "response_format", "responseFormat": "response_format", "text": "text", "verbosity": "text.verbosity", "truncation": "truncation", "tool_choice": "tool_choice", "toolChoice": "tool_choice", "service_tier": "service_tier", "serviceTier": "service_tier", } for sourcePath, targetPath := range copyRawPaths { if value := cfg.Get(sourcePath); value.Exists() { out, _ = sjson.SetRawBytes(out, targetPath, []byte(value.Raw)) } } return out } func interactionsCodexReasoningEffort(cfg gjson.Result) string { for _, path := range []string{ "thinking_level", "thinkingLevel", "thinking_config.thinking_level", "thinking_config.thinkingLevel", "thinkingConfig.thinking_level", "thinkingConfig.thinkingLevel", "reasoning.effort", } { if value := cfg.Get(path); value.Exists() { effort := strings.ToLower(strings.TrimSpace(value.String())) if effort != "" { return effort } } } for _, path := range []string{ "thinking_budget", "thinkingBudget", "thinking_config.thinking_budget", "thinking_config.thinkingBudget", "thinkingConfig.thinking_budget", "thinkingConfig.thinkingBudget", } { if value := cfg.Get(path); value.Exists() { if effort, ok := thinking.ConvertBudgetToLevel(int(value.Int())); ok { return effort } } } return "" } func interactionsCodexReasoningSummary(cfg gjson.Result) string { for _, path := range []string{ "thinking_summaries", "thinkingSummaries", "reasoning.summary", } { if value := cfg.Get(path); value.Exists() { switch value.Type { case gjson.True: return "auto" case gjson.False: return "none" case gjson.String: summary := strings.ToLower(strings.TrimSpace(value.String())) if summary != "" { return summary } } } } for _, path := range []string{ "include_thoughts", "includeThoughts", "thinking_config.include_thoughts", "thinking_config.includeThoughts", "thinkingConfig.include_thoughts", "thinkingConfig.includeThoughts", } { if value := cfg.Get(path); value.Exists() { if value.Bool() { return "auto" } return "none" } } return "" } func appendInteractionsInputToCodex(out []byte, input gjson.Result) []byte { if !input.Exists() { return out } if input.Type == gjson.String { return appendInteractionsTextToCodex(out, "user", input.String()) } if input.IsArray() { input.ForEach(func(_, step gjson.Result) bool { out = appendInteractionsStepToCodex(out, step, "user") return true }) return out } if steps := input.Get("steps"); steps.Exists() && steps.IsArray() { defaultRole := interactionsCodexDefaultRole(input.Get("role").String(), "user") steps.ForEach(func(_, step gjson.Result) bool { out = appendInteractionsStepToCodex(out, step, defaultRole) return true }) return out } return appendInteractionsStepToCodex(out, input, "user") } func appendInteractionsStepToCodex(out []byte, step gjson.Result, defaultRole string) []byte { if step.Type == gjson.String { return appendInteractionsTextToCodex(out, defaultRole, step.String()) } if steps := step.Get("steps"); steps.Exists() && steps.IsArray() { role := interactionsCodexDefaultRole(step.Get("role").String(), defaultRole) steps.ForEach(func(_, nested gjson.Result) bool { out = appendInteractionsStepToCodex(out, nested, role) return true }) return out } stepType := strings.ToLower(strings.TrimSpace(step.Get("type").String())) switch stepType { case "function_call": return appendInteractionsFunctionCallToCodex(out, step) case "function_result", "function_call_output": return appendInteractionsFunctionResultToCodex(out, step) case "model_output", "assistant": return appendInteractionsContentToCodexItem(out, step.Get("content"), "assistant") case "thought", "reasoning": return appendInteractionsThoughtToCodex(out, step) case "user_input", "message", "": role := interactionsCodexDefaultRole(step.Get("role").String(), defaultRole) if content := step.Get("content"); content.Exists() { return appendInteractionsContentToCodexItem(out, content, role) } if text := step.Get("text"); text.Exists() { return appendInteractionsTextToCodex(out, role, text.String()) } default: role := interactionsCodexDefaultRole(step.Get("role").String(), defaultRole) if content := step.Get("content"); content.Exists() { return appendInteractionsContentToCodexItem(out, content, role) } if text := step.Get("text"); text.Exists() { return appendInteractionsTextToCodex(out, role, text.String()) } } return out } func appendInteractionsContentToCodexItem(out []byte, content gjson.Result, role string) []byte { if !content.Exists() { return out } if content.Type == gjson.String { return appendInteractionsTextToCodex(out, role, content.String()) } if content.IsArray() { content.ForEach(func(_, part gjson.Result) bool { item := interactionsCodexMessagePart(part, role) if len(item) > 0 { out = appendInteractionsMessagePartToCodex(out, role, item) } return true }) return out } if content.IsObject() { if item := interactionsCodexMessagePart(content, role); len(item) > 0 { return appendInteractionsMessagePartToCodex(out, role, item) } } return out } func appendInteractionsFunctionCallToCodex(out []byte, step gjson.Result) []byte { item := []byte(`{"type":"function_call"}`) if name := step.Get("name"); name.Exists() { item, _ = sjson.SetBytes(item, "name", shortenCodexToolNameIfNeeded(name.String())) } if callID := interactionsCodexCallID(step); callID != "" { item, _ = sjson.SetBytes(item, "call_id", callID) } if args := step.Get("arguments"); args.Exists() { item, _ = sjson.SetBytes(item, "arguments", interactionsCodexJSONString(args)) } else if args := step.Get("args"); args.Exists() { item, _ = sjson.SetBytes(item, "arguments", interactionsCodexJSONString(args)) } out, _ = sjson.SetRawBytes(out, "input.-1", item) return out } func appendInteractionsFunctionResultToCodex(out []byte, step gjson.Result) []byte { item := []byte(`{"type":"function_call_output"}`) if callID := interactionsCodexCallID(step); callID != "" { item, _ = sjson.SetBytes(item, "call_id", callID) } if result := step.Get("result"); result.Exists() { item, _ = sjson.SetBytes(item, "output", interactionsCodexOutputString(result)) } else if output := step.Get("output"); output.Exists() { item, _ = sjson.SetBytes(item, "output", interactionsCodexOutputString(output)) } out, _ = sjson.SetRawBytes(out, "input.-1", item) return out } func copyInteractionsToolsToCodex(out []byte, root gjson.Result) []byte { tools := root.Get("tools") if !tools.Exists() { return out } if !tools.IsArray() { out, _ = sjson.SetRawBytes(out, "tools", []byte(tools.Raw)) return out } normalized := make([]map[string]any, 0) tools.ForEach(func(_, tool gjson.Result) bool { if decls := tool.Get("function_declarations"); decls.Exists() { appendCodexToolDeclarations(&normalized, decls) return true } if decls := tool.Get("functionDeclarations"); decls.Exists() { appendCodexToolDeclarations(&normalized, decls) return true } if name := tool.Get("name"); name.Exists() { normalized = append(normalized, codexToolFromDeclaration(tool)) } return true }) if len(normalized) == 0 { out, _ = sjson.SetRawBytes(out, "tools", []byte(tools.Raw)) return out } raw, errMarshal := json.Marshal(normalized) if errMarshal != nil { out, _ = sjson.SetRawBytes(out, "tools", []byte(tools.Raw)) return out } out, _ = sjson.SetRawBytes(out, "tools", raw) if !gjson.GetBytes(out, "tool_choice").Exists() { out, _ = sjson.SetBytes(out, "tool_choice", "auto") } return out } func copyInteractionsCodexTopLevel(out []byte, root gjson.Result) []byte { if serviceTier := normalizeInteractionsCodexServiceTier(root.Get("service_tier")); serviceTier != "" { out, _ = sjson.SetBytes(out, "service_tier", serviceTier) } if toolChoice := root.Get("tool_choice"); toolChoice.Exists() { out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(toolChoice.Raw)) } for _, path := range []string{"parallel_tool_calls", "store", "metadata", "include", "truncation"} { if value := root.Get(path); value.Exists() { out, _ = sjson.SetRawBytes(out, path, []byte(value.Raw)) } } return out } func appendInteractionsThoughtToCodex(out []byte, step gjson.Result) []byte { text := interactionsCodexContentText(step.Get("content")) if text == "" { text = step.Get("text").String() } item := []byte(`{"type":"reasoning"}`) if text != "" { item, _ = sjson.SetBytes(item, "content", text) } if id := step.Get("id"); id.Exists() { item, _ = sjson.SetBytes(item, "id", id.String()) } out, _ = sjson.SetRawBytes(out, "input.-1", item) return out } func appendInteractionsTextToCodex(out []byte, role, text string) []byte { part := []byte(`{"type":"","text":""}`) if role == "assistant" { part, _ = sjson.SetBytes(part, "type", "output_text") } else { part, _ = sjson.SetBytes(part, "type", "input_text") } part, _ = sjson.SetBytes(part, "text", text) return appendInteractionsMessagePartToCodex(out, role, part) } func appendInteractionsMessagePartToCodex(out []byte, role string, part []byte) []byte { message := []byte(`{"type":"message","role":"","content":[]}`) message, _ = sjson.SetBytes(message, "role", role) message, _ = sjson.SetRawBytes(message, "content.-1", part) out, _ = sjson.SetRawBytes(out, "input.-1", message) return out } func interactionsCodexMessagePart(part gjson.Result, role string) []byte { if text := part.Get("text"); text.Exists() { item := []byte(`{"type":"","text":""}`) if role == "assistant" { item, _ = sjson.SetBytes(item, "type", "output_text") } else { item, _ = sjson.SetBytes(item, "type", "input_text") } item, _ = sjson.SetBytes(item, "text", text.String()) return item } partType := strings.ToLower(strings.TrimSpace(part.Get("type").String())) switch partType { case "text", "": return nil case "image": return interactionsCodexImagePart(part) case "image_url": item := []byte(`{"type":"input_image","image_url":""}`) item, _ = sjson.SetBytes(item, "image_url", part.Get("image_url.url").String()) return item case "audio": return interactionsCodexAudioPart(part) case "input_audio": item := []byte(`{"type":"input_audio","input_audio":{}}`) if audio := part.Get("input_audio"); audio.Exists() { item, _ = sjson.SetRawBytes(item, "input_audio", []byte(audio.Raw)) } return item case "video", "document", "file": return interactionsCodexFilePart(part) default: if inline := part.Get("inline_data"); inline.Exists() { return interactionsCodexInlinePart(inline) } if inline := part.Get("inlineData"); inline.Exists() { return interactionsCodexInlinePart(inline) } if file := part.Get("file_data"); file.Exists() { return interactionsCodexFileDataPart(file) } if file := part.Get("fileData"); file.Exists() { return interactionsCodexFileDataPart(file) } } return nil } func interactionsCodexImagePart(part gjson.Result) []byte { if url := part.Get("url"); url.Exists() { item := []byte(`{"type":"input_image","image_url":""}`) item, _ = sjson.SetBytes(item, "image_url", url.String()) return item } if fileURI := firstString(part, "file_uri", "fileUri"); fileURI != "" { item := []byte(`{"type":"input_image","image_url":""}`) item, _ = sjson.SetBytes(item, "image_url", fileURI) return item } mimeType := firstString(part, "mime_type", "mimeType") data := part.Get("data").String() if mimeType == "" || data == "" { return nil } item := []byte(`{"type":"input_image","image_url":""}`) item, _ = sjson.SetBytes(item, "image_url", fmt.Sprintf("data:%s;base64,%s", mimeType, data)) return item } func interactionsCodexAudioPart(part gjson.Result) []byte { mimeType := firstString(part, "mime_type", "mimeType") data := part.Get("data").String() if mimeType == "" || data == "" { return nil } item := []byte(`{"type":"input_audio","input_audio":{"data":"","format":""}}`) item, _ = sjson.SetBytes(item, "input_audio.data", data) item, _ = sjson.SetBytes(item, "input_audio.format", codexInputAudioFormatFromMIME(mimeType)) return item } func interactionsCodexFilePart(part gjson.Result) []byte { if fileData := part.Get("file.file_data").String(); fileData != "" { item := []byte(`{"type":"input_file","file_data":"","filename":""}`) item, _ = sjson.SetBytes(item, "file_data", fileData) item, _ = sjson.SetBytes(item, "filename", part.Get("file.filename").String()) return item } mimeType := firstString(part, "mime_type", "mimeType") if fileURI := firstString(part, "file_uri", "fileUri", "url"); fileURI != "" { item := []byte(`{"type":"input_file","file_url":"","filename":""}`) item, _ = sjson.SetBytes(item, "file_url", fileURI) item, _ = sjson.SetBytes(item, "filename", codexFileNameFromMIME(mimeType)) return item } data := part.Get("data").String() if mimeType == "" || data == "" { return nil } item := []byte(`{"type":"input_file","file_data":"","filename":""}`) item, _ = sjson.SetBytes(item, "file_data", data) item, _ = sjson.SetBytes(item, "filename", codexFileNameFromMIME(mimeType)) return item } func interactionsCodexInlinePart(inline gjson.Result) []byte { mimeType := firstString(inline, "mime_type", "mimeType") data := inline.Get("data").String() if mimeType == "" || data == "" { return nil } switch { case strings.HasPrefix(strings.ToLower(mimeType), "image/"): return interactionsCodexImagePart(gjson.Parse(fmt.Sprintf(`{"mime_type":%q,"data":%q}`, mimeType, data))) case strings.HasPrefix(strings.ToLower(mimeType), "audio/"): return interactionsCodexAudioPart(gjson.Parse(fmt.Sprintf(`{"mime_type":%q,"data":%q}`, mimeType, data))) default: return interactionsCodexFilePart(gjson.Parse(fmt.Sprintf(`{"mime_type":%q,"data":%q}`, mimeType, data))) } } func interactionsCodexFileDataPart(fileData gjson.Result) []byte { mimeType := firstString(fileData, "mime_type", "mimeType") fileURI := firstString(fileData, "file_uri", "fileUri") if fileURI == "" { return nil } if strings.HasPrefix(strings.ToLower(mimeType), "image/") { item := []byte(`{"type":"input_image","image_url":""}`) item, _ = sjson.SetBytes(item, "image_url", fileURI) return item } item := []byte(`{"type":"input_file","file_url":"","filename":""}`) item, _ = sjson.SetBytes(item, "file_url", fileURI) item, _ = sjson.SetBytes(item, "filename", codexFileNameFromMIME(mimeType)) return item } func appendCodexToolDeclarations(normalized *[]map[string]any, declarations gjson.Result) { if !declarations.IsArray() { return } declarations.ForEach(func(_, declaration gjson.Result) bool { if declaration.Get("name").Exists() { *normalized = append(*normalized, codexToolFromDeclaration(declaration)) } return true }) } func codexToolFromDeclaration(declaration gjson.Result) map[string]any { tool := map[string]any{ "type": "function", "name": shortenCodexToolNameIfNeeded(declaration.Get("name").String()), "strict": false, } if desc := declaration.Get("description"); desc.Exists() { tool["description"] = desc.String() } if params := declaration.Get("parameters"); params.Exists() { tool["parameters"] = cleanedCodexToolParameters(params) } else if params := declaration.Get("parametersJsonSchema"); params.Exists() { tool["parameters"] = cleanedCodexToolParameters(params) } else if params := declaration.Get("parameters_json_schema"); params.Exists() { tool["parameters"] = cleanedCodexToolParameters(params) } return tool } func cleanedCodexToolParameters(params gjson.Result) json.RawMessage { cleaned := []byte(params.Raw) cleaned, _ = sjson.DeleteBytes(cleaned, "$schema") cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false) return json.RawMessage(cleaned) } func interactionsCodexContentText(content gjson.Result) string { if !content.Exists() { return "" } if content.Type == gjson.String { return content.String() } if content.IsObject() { return content.Get("text").String() } if content.IsArray() { var builder strings.Builder content.ForEach(func(_, part gjson.Result) bool { text := part.Get("text").String() if text == "" { return true } if builder.Len() > 0 { builder.WriteByte('\n') } builder.WriteString(text) return true }) return builder.String() } return "" } func interactionsCodexCallID(step gjson.Result) string { if callID := strings.TrimSpace(step.Get("call_id").String()); callID != "" { return callID } return strings.TrimSpace(step.Get("id").String()) } func interactionsCodexJSONString(value gjson.Result) string { if value.Type == gjson.String { return value.String() } if value.Exists() { return value.Raw } return "{}" } func interactionsCodexOutputString(value gjson.Result) string { if value.Type == gjson.String { return value.String() } if value.Exists() { return value.Raw } return "" } func interactionsCodexDefaultRole(role, fallback string) string { switch strings.ToLower(strings.TrimSpace(role)) { case "model", "assistant": return "assistant" case "developer", "system": return "developer" case "user": return "user" } if fallback == "assistant" || fallback == "developer" { return fallback } return "user" } func normalizeInteractionsCodexServiceTier(serviceTier gjson.Result) string { if !serviceTier.Exists() || serviceTier.Type != gjson.String { return "" } switch strings.ToLower(strings.TrimSpace(serviceTier.String())) { case "priority", "fast": return "priority" } return "" } func codexInputAudioFormatFromMIME(mimeType string) string { switch strings.ToLower(strings.TrimSpace(mimeType)) { case "audio/wav", "audio/wave", "audio/x-wav": return "wav" case "audio/flac": return "flac" case "audio/opus", "audio/ogg": return "opus" case "audio/pcm", "audio/l16": return "pcm16" default: return "mp3" } } func codexFileNameFromMIME(mimeType string) string { switch strings.ToLower(strings.TrimSpace(mimeType)) { case "application/pdf": return "document.pdf" case "text/plain": return "document.txt" case "text/csv": return "document.csv" case "application/json": return "document.json" case "application/xml", "text/xml": return "document.xml" default: if strings.HasPrefix(strings.ToLower(strings.TrimSpace(mimeType)), "video/") { return "video" } return "document" } } func shortenCodexToolNameIfNeeded(name string) string { const limit = 64 if len(name) <= limit { return name } if strings.HasPrefix(name, "mcp__") { idx := strings.LastIndex(name, "__") if idx > 0 { candidate := "mcp__" + name[idx+2:] if len(candidate) > limit { return candidate[:limit] } return candidate } } return name[:limit] } func firstString(root gjson.Result, paths ...string) string { for _, path := range paths { if value := root.Get(path); value.Exists() { return value.String() } } return "" }