718 lines
22 KiB
Go
718 lines
22 KiB
Go
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 ""
|
|
}
|