Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
19
backend/internal/translator/codex/interactions/init.go
Normal file
19
backend/internal/translator/codex/interactions/init.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
. "github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
Interactions,
|
||||
Codex,
|
||||
ConvertInteractionsRequestToCodex,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertCodexResponseToInteractions,
|
||||
NonStream: ConvertCodexResponseToInteractionsNonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,727 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
||||
"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)
|
||||
inputItems := translatorcommon.NewRawArrayItems(root.Get("input.#").Int())
|
||||
appendInteractionsInputToCodex(&inputItems, root.Get("input"))
|
||||
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
|
||||
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.Type == gjson.String {
|
||||
summary := strings.ToLower(strings.TrimSpace(value.String()))
|
||||
switch summary {
|
||||
case "auto", "none":
|
||||
return summary
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, path := range []string{
|
||||
"include_thoughts",
|
||||
"includeThoughts",
|
||||
"thinking_config.include_thoughts",
|
||||
"thinking_config.includeThoughts",
|
||||
"thinkingConfig.include_thoughts",
|
||||
"thinkingConfig.includeThoughts",
|
||||
} {
|
||||
switch value := cfg.Get(path); value.Type {
|
||||
case gjson.True:
|
||||
return "auto"
|
||||
case gjson.False:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func appendInteractionsInputToCodex(items *[][]byte, input gjson.Result) {
|
||||
if !input.Exists() {
|
||||
return
|
||||
}
|
||||
if input.Type == gjson.String {
|
||||
appendInteractionsTextToCodex(items, "user", input.String())
|
||||
return
|
||||
}
|
||||
if input.IsArray() {
|
||||
input.ForEach(func(_, step gjson.Result) bool {
|
||||
appendInteractionsStepToCodex(items, step, "user")
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
if steps := input.Get("steps"); steps.Exists() && steps.IsArray() {
|
||||
defaultRole := interactionsCodexDefaultRole(input.Get("role").String(), "user")
|
||||
steps.ForEach(func(_, step gjson.Result) bool {
|
||||
appendInteractionsStepToCodex(items, step, defaultRole)
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
appendInteractionsStepToCodex(items, input, "user")
|
||||
}
|
||||
|
||||
func appendInteractionsStepToCodex(items *[][]byte, step gjson.Result, defaultRole string) {
|
||||
if step.Type == gjson.String {
|
||||
appendInteractionsTextToCodex(items, defaultRole, step.String())
|
||||
return
|
||||
}
|
||||
if steps := step.Get("steps"); steps.Exists() && steps.IsArray() {
|
||||
role := interactionsCodexDefaultRole(step.Get("role").String(), defaultRole)
|
||||
steps.ForEach(func(_, nested gjson.Result) bool {
|
||||
appendInteractionsStepToCodex(items, nested, role)
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
stepType := strings.ToLower(strings.TrimSpace(step.Get("type").String()))
|
||||
switch stepType {
|
||||
case "function_call":
|
||||
appendInteractionsFunctionCallToCodex(items, step)
|
||||
case "function_result", "function_call_output":
|
||||
appendInteractionsFunctionResultToCodex(items, step)
|
||||
case "model_output", "assistant":
|
||||
appendInteractionsContentToCodexItem(items, step.Get("content"), "assistant")
|
||||
case "thought", "reasoning":
|
||||
appendInteractionsThoughtToCodex(items, step)
|
||||
case "user_input", "message", "":
|
||||
role := interactionsCodexDefaultRole(step.Get("role").String(), defaultRole)
|
||||
if content := step.Get("content"); content.Exists() {
|
||||
appendInteractionsContentToCodexItem(items, content, role)
|
||||
} else if text := step.Get("text"); text.Exists() {
|
||||
appendInteractionsTextToCodex(items, role, text.String())
|
||||
}
|
||||
default:
|
||||
role := interactionsCodexDefaultRole(step.Get("role").String(), defaultRole)
|
||||
if content := step.Get("content"); content.Exists() {
|
||||
appendInteractionsContentToCodexItem(items, content, role)
|
||||
} else if text := step.Get("text"); text.Exists() {
|
||||
appendInteractionsTextToCodex(items, role, text.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendInteractionsContentToCodexItem(items *[][]byte, content gjson.Result, role string) {
|
||||
if !content.Exists() {
|
||||
return
|
||||
}
|
||||
if content.Type == gjson.String {
|
||||
appendInteractionsTextToCodex(items, role, content.String())
|
||||
return
|
||||
}
|
||||
if content.IsArray() {
|
||||
content.ForEach(func(_, part gjson.Result) bool {
|
||||
if item := interactionsCodexMessagePart(part, role); len(item) > 0 {
|
||||
appendInteractionsMessagePartToCodex(items, role, item)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
if content.IsObject() {
|
||||
if item := interactionsCodexMessagePart(content, role); len(item) > 0 {
|
||||
appendInteractionsMessagePartToCodex(items, role, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendInteractionsFunctionCallToCodex(items *[][]byte, step gjson.Result) {
|
||||
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))
|
||||
}
|
||||
*items = append(*items, item)
|
||||
}
|
||||
|
||||
func appendInteractionsFunctionResultToCodex(items *[][]byte, step gjson.Result) {
|
||||
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))
|
||||
}
|
||||
*items = append(*items, item)
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
current := gjson.GetBytes(out, "service_tier")
|
||||
if current.Type != gjson.String || current.String() != serviceTier {
|
||||
out, _ = sjson.SetBytes(out, "service_tier", serviceTier)
|
||||
}
|
||||
}
|
||||
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
|
||||
out = setInteractionsCodexRawIfDifferent(out, "tool_choice", toolChoice)
|
||||
}
|
||||
for _, path := range []string{"parallel_tool_calls", "store", "metadata", "include", "truncation"} {
|
||||
if value := root.Get(path); value.Exists() {
|
||||
out = setInteractionsCodexRawIfDifferent(out, path, value)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func setInteractionsCodexRawIfDifferent(out []byte, path string, value gjson.Result) []byte {
|
||||
current := gjson.GetBytes(out, path)
|
||||
if current.Exists() && current.Raw == value.Raw {
|
||||
return out
|
||||
}
|
||||
updated, errSet := sjson.SetRawBytes(out, path, []byte(value.Raw))
|
||||
if errSet != nil {
|
||||
return out
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func appendInteractionsThoughtToCodex(items *[][]byte, step gjson.Result) {
|
||||
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())
|
||||
}
|
||||
*items = append(*items, item)
|
||||
}
|
||||
|
||||
func appendInteractionsTextToCodex(items *[][]byte, role, text string) {
|
||||
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)
|
||||
appendInteractionsMessagePartToCodex(items, role, part)
|
||||
}
|
||||
|
||||
func appendInteractionsMessagePartToCodex(items *[][]byte, role string, part []byte) {
|
||||
message := []byte(`{"type":"message","role":"","content":[]}`)
|
||||
message, _ = sjson.SetBytes(message, "role", role)
|
||||
message, _ = sjson.SetRawBytes(message, "content", translatorcommon.JoinRawArray([][]byte{part}))
|
||||
*items = append(*items, message)
|
||||
}
|
||||
|
||||
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)
|
||||
if params.Get("$schema").Exists() {
|
||||
cleaned, _ = sjson.DeleteBytes(cleaned, "$schema")
|
||||
}
|
||||
if params.Get("additionalProperties").Type != gjson.False {
|
||||
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 ""
|
||||
}
|
||||
|
|
@ -0,0 +1,595 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
type codexToInteractionsStreamState struct {
|
||||
Started bool
|
||||
Completed bool
|
||||
Done bool
|
||||
ActiveStepOpen bool
|
||||
ActiveStepType string
|
||||
ActiveStepIndex int
|
||||
StepIndex int
|
||||
ID string
|
||||
Model string
|
||||
CreatedAt int64
|
||||
HasOutputText bool
|
||||
FunctionCallName string
|
||||
FunctionCallID string
|
||||
}
|
||||
|
||||
func ConvertCodexResponseToInteractions(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||
_ = ctx
|
||||
_ = originalRequestRawJSON
|
||||
_ = requestRawJSON
|
||||
if param == nil {
|
||||
var local any
|
||||
param = &local
|
||||
}
|
||||
if *param == nil {
|
||||
*param = &codexToInteractionsStreamState{
|
||||
ID: fmt.Sprintf("interaction_%d", time.Now().UnixNano()),
|
||||
Model: modelName,
|
||||
}
|
||||
}
|
||||
st := (*param).(*codexToInteractionsStreamState)
|
||||
payload := codexStreamPayload(rawJSON)
|
||||
if bytes.Equal(payload, []byte("[DONE]")) {
|
||||
out := appendCodexInteractionsStepStop(nil, st)
|
||||
if !st.Completed {
|
||||
out = appendCodexInteractionsCompleted(out, st, gjson.Result{})
|
||||
}
|
||||
return appendCodexInteractionsDone(out, st)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
root := gjson.ParseBytes(payload)
|
||||
switch root.Get("type").String() {
|
||||
case "response.created":
|
||||
return appendCodexInteractionsCreated(nil, st, root.Get("response"))
|
||||
case "response.output_item.added":
|
||||
return codexOutputItemAddedToInteractions(st, root)
|
||||
case "response.output_text.delta":
|
||||
return codexOutputTextDeltaToInteractions(st, root)
|
||||
case "response.reasoning_summary_text.delta", "response.reasoning_text.delta":
|
||||
return codexReasoningDeltaToInteractions(st, root)
|
||||
case "response.function_call_arguments.delta":
|
||||
return codexFunctionArgumentsDeltaToInteractions(st, root)
|
||||
case "response.output_item.done":
|
||||
return codexOutputItemDoneToInteractions(st, root.Get("item"))
|
||||
case "response.completed", "response.incomplete":
|
||||
out := appendCodexInteractionsCreated(nil, st, root.Get("response"))
|
||||
out = appendCodexInteractionsStepStop(out, st)
|
||||
out = appendCodexInteractionsCompleted(out, st, root.Get("response"))
|
||||
return appendCodexInteractionsDone(out, st)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertCodexResponseToInteractionsNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||
_ = ctx
|
||||
_ = originalRequestRawJSON
|
||||
_ = requestRawJSON
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
response := root.Get("response")
|
||||
if !response.Exists() {
|
||||
response = root
|
||||
}
|
||||
out := []byte(`{"id":"","object":"interaction","status":"completed","model":"","steps":[]}`)
|
||||
if status := response.Get("status").String(); status != "" {
|
||||
out, _ = sjson.SetBytes(out, "status", status)
|
||||
}
|
||||
id := response.Get("id").String()
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("interaction_%d", time.Now().UnixNano())
|
||||
}
|
||||
out, _ = sjson.SetBytes(out, "id", id)
|
||||
if model := response.Get("model").String(); model != "" {
|
||||
out, _ = sjson.SetBytes(out, "model", model)
|
||||
} else {
|
||||
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||
}
|
||||
var steps [][]byte
|
||||
response.Get("output").ForEach(func(_, item gjson.Result) bool {
|
||||
switch item.Get("type").String() {
|
||||
case "message":
|
||||
if step := buildCodexMessageItemToInteractions(item); len(step) > 0 {
|
||||
steps = append(steps, step)
|
||||
}
|
||||
case "reasoning":
|
||||
if step := buildCodexReasoningItemToInteractions(item); len(step) > 0 {
|
||||
steps = append(steps, step)
|
||||
}
|
||||
case "function_call", "tool_call":
|
||||
if step := buildCodexFunctionCallItemToInteractions(item); len(step) > 0 {
|
||||
steps = append(steps, step)
|
||||
}
|
||||
case "image_generation_call":
|
||||
if step := buildCodexImageItemToInteractions(item); len(step) > 0 {
|
||||
steps = append(steps, step)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(steps) > 0 {
|
||||
out = translatorcommon.SetRawArrayItems(out, "steps", steps)
|
||||
}
|
||||
out = setCodexInteractionsUsage(out, "usage", response.Get("usage"), false)
|
||||
return out
|
||||
}
|
||||
|
||||
func codexStreamPayload(rawJSON []byte) []byte {
|
||||
rawJSON = bytes.TrimSpace(rawJSON)
|
||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||
rawJSON = bytes.TrimSpace(rawJSON[len("data:"):])
|
||||
}
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
func codexStreamEventType(rawJSON []byte) string {
|
||||
payload := codexStreamPayload(rawJSON)
|
||||
if len(payload) == 0 || bytes.Equal(payload, []byte("[DONE]")) {
|
||||
return ""
|
||||
}
|
||||
return gjson.GetBytes(payload, "type").String()
|
||||
}
|
||||
|
||||
func appendCodexInteractionsCreated(out [][]byte, st *codexToInteractionsStreamState, response gjson.Result) [][]byte {
|
||||
if st.Started {
|
||||
return out
|
||||
}
|
||||
if id := response.Get("id").String(); id != "" {
|
||||
st.ID = id
|
||||
}
|
||||
if model := response.Get("model").String(); model != "" {
|
||||
st.Model = model
|
||||
}
|
||||
if createdAt := response.Get("created_at"); createdAt.Exists() {
|
||||
st.CreatedAt = createdAt.Int()
|
||||
}
|
||||
created := []byte(`{"interaction":{"id":"","status":"in_progress","object":"interaction","model":""},"event_type":"interaction.created"}`)
|
||||
created, _ = sjson.SetBytes(created, "interaction.id", st.ID)
|
||||
created, _ = sjson.SetBytes(created, "interaction.model", st.Model)
|
||||
out = append(out, translatorcommon.SSEEventData("interaction.created", created))
|
||||
statusUpdate := []byte(`{"interaction_id":"","status":"in_progress","event_type":"interaction.status_update"}`)
|
||||
statusUpdate, _ = sjson.SetBytes(statusUpdate, "interaction_id", st.ID)
|
||||
out = append(out, translatorcommon.SSEEventData("interaction.status_update", statusUpdate))
|
||||
st.Started = true
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCodexInteractionsCompleted(out [][]byte, st *codexToInteractionsStreamState, response gjson.Result) [][]byte {
|
||||
if st.Completed {
|
||||
return out
|
||||
}
|
||||
created := time.Now().UTC()
|
||||
if st.CreatedAt > 0 {
|
||||
created = time.Unix(st.CreatedAt, 0).UTC()
|
||||
}
|
||||
completed := []byte(`{"interaction":{"id":"","status":"completed","usage":{},"created":"","updated":"","service_tier":"standard","object":"interaction","model":""},"event_type":"interaction.completed"}`)
|
||||
completed, _ = sjson.SetBytes(completed, "interaction.id", st.ID)
|
||||
completed, _ = sjson.SetBytes(completed, "interaction.created", created.Format(time.RFC3339))
|
||||
completed, _ = sjson.SetBytes(completed, "interaction.updated", time.Now().UTC().Format(time.RFC3339))
|
||||
completed, _ = sjson.SetBytes(completed, "interaction.model", st.Model)
|
||||
if status := response.Get("status").String(); status != "" {
|
||||
completed, _ = sjson.SetBytes(completed, "interaction.status", status)
|
||||
}
|
||||
completed = setCodexInteractionsUsage(completed, "interaction.usage", response.Get("usage"), true)
|
||||
out = append(out, translatorcommon.SSEEventData("interaction.completed", completed))
|
||||
st.Completed = true
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCodexInteractionsDone(out [][]byte, st *codexToInteractionsStreamState) [][]byte {
|
||||
if st.Done {
|
||||
return out
|
||||
}
|
||||
out = append(out, translatorcommon.SSEEventData("done", []byte("[DONE]")))
|
||||
st.Done = true
|
||||
return out
|
||||
}
|
||||
|
||||
func codexOutputItemAddedToInteractions(st *codexToInteractionsStreamState, root gjson.Result) [][]byte {
|
||||
out := appendCodexInteractionsCreated(nil, st, root.Get("response"))
|
||||
item := root.Get("item")
|
||||
switch item.Get("type").String() {
|
||||
case "message":
|
||||
return ensureCodexInteractionsStep(out, st, "model_output", item)
|
||||
case "reasoning":
|
||||
return ensureCodexInteractionsStep(out, st, "thought", item)
|
||||
case "function_call", "tool_call":
|
||||
st.FunctionCallName = item.Get("name").String()
|
||||
st.FunctionCallID = codexItemCallID(item)
|
||||
return ensureCodexInteractionsStep(out, st, "function_call", item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func codexOutputTextDeltaToInteractions(st *codexToInteractionsStreamState, root gjson.Result) [][]byte {
|
||||
out := appendCodexInteractionsCreated(nil, st, root.Get("response"))
|
||||
out = ensureCodexInteractionsStep(out, st, "model_output", gjson.Result{})
|
||||
delta := []byte(`{"index":0,"delta":{"text":"","type":"text"},"event_type":"step.delta"}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.text", root.Get("delta").String())
|
||||
st.HasOutputText = true
|
||||
return append(out, translatorcommon.SSEEventData("step.delta", delta))
|
||||
}
|
||||
|
||||
func codexReasoningDeltaToInteractions(st *codexToInteractionsStreamState, root gjson.Result) [][]byte {
|
||||
out := appendCodexInteractionsCreated(nil, st, root.Get("response"))
|
||||
out = ensureCodexInteractionsStep(out, st, "thought", gjson.Result{})
|
||||
delta := []byte(`{"index":0,"delta":{"content":{"text":"","type":"text"},"type":"thought_summary"},"event_type":"step.delta"}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.content.text", root.Get("delta").String())
|
||||
return append(out, translatorcommon.SSEEventData("step.delta", delta))
|
||||
}
|
||||
|
||||
func codexFunctionArgumentsDeltaToInteractions(st *codexToInteractionsStreamState, root gjson.Result) [][]byte {
|
||||
out := appendCodexInteractionsCreated(nil, st, root.Get("response"))
|
||||
out = ensureCodexInteractionsStep(out, st, "function_call", root.Get("item"))
|
||||
delta := []byte(`{"index":0,"delta":{"arguments":"","type":"arguments_delta"},"event_type":"step.delta"}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.arguments", root.Get("delta").String())
|
||||
return append(out, translatorcommon.SSEEventData("step.delta", delta))
|
||||
}
|
||||
|
||||
func codexOutputItemDoneToInteractions(st *codexToInteractionsStreamState, item gjson.Result) [][]byte {
|
||||
out := appendCodexInteractionsCreated(nil, st, gjson.Result{})
|
||||
switch item.Get("type").String() {
|
||||
case "message":
|
||||
if st.HasOutputText {
|
||||
return appendCodexInteractionsStepStop(out, st)
|
||||
}
|
||||
out = appendCodexMessageItemToInteractionsStream(out, st, item)
|
||||
return appendCodexInteractionsStepStop(out, st)
|
||||
case "reasoning":
|
||||
out = appendCodexReasoningItemToInteractionsStream(out, st, item)
|
||||
return appendCodexInteractionsStepStop(out, st)
|
||||
case "function_call", "tool_call":
|
||||
out = appendCodexFunctionCallItemToInteractionsStream(out, st, item)
|
||||
return appendCodexInteractionsStepStop(out, st)
|
||||
case "image_generation_call":
|
||||
out = appendCodexImageItemToInteractionsStream(out, st, item)
|
||||
return appendCodexInteractionsStepStop(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ensureCodexInteractionsStep(out [][]byte, st *codexToInteractionsStreamState, stepType string, item gjson.Result) [][]byte {
|
||||
if st.ActiveStepOpen && st.ActiveStepType == stepType {
|
||||
return out
|
||||
}
|
||||
out = appendCodexInteractionsStepStop(out, st)
|
||||
return appendCodexInteractionsStepStart(out, st, stepType, item)
|
||||
}
|
||||
|
||||
func appendCodexInteractionsStepStart(out [][]byte, st *codexToInteractionsStreamState, stepType string, item gjson.Result) [][]byte {
|
||||
st.ActiveStepIndex = st.StepIndex
|
||||
st.StepIndex++
|
||||
st.ActiveStepOpen = true
|
||||
st.ActiveStepType = stepType
|
||||
stepStart := []byte(`{"index":0,"step":{"type":""},"event_type":"step.start"}`)
|
||||
stepStart, _ = sjson.SetBytes(stepStart, "index", st.ActiveStepIndex)
|
||||
stepStart, _ = sjson.SetBytes(stepStart, "step.type", stepType)
|
||||
if stepType == "function_call" {
|
||||
name := item.Get("name").String()
|
||||
if name == "" {
|
||||
name = st.FunctionCallName
|
||||
}
|
||||
callID := codexItemCallID(item)
|
||||
if callID == "" {
|
||||
callID = st.FunctionCallID
|
||||
}
|
||||
if callID == "" {
|
||||
callID = fmt.Sprintf("step_%d", time.Now().UnixNano())
|
||||
}
|
||||
stepStart, _ = sjson.SetBytes(stepStart, "step.id", callID)
|
||||
stepStart, _ = sjson.SetBytes(stepStart, "step.call_id", callID)
|
||||
stepStart, _ = sjson.SetBytes(stepStart, "step.name", name)
|
||||
stepStart, _ = sjson.SetRawBytes(stepStart, "step.arguments", []byte(`{}`))
|
||||
}
|
||||
return append(out, translatorcommon.SSEEventData("step.start", stepStart))
|
||||
}
|
||||
|
||||
func appendCodexInteractionsStepStop(out [][]byte, st *codexToInteractionsStreamState) [][]byte {
|
||||
if !st.ActiveStepOpen {
|
||||
return out
|
||||
}
|
||||
stepStop := []byte(`{"index":0,"event_type":"step.stop"}`)
|
||||
stepStop, _ = sjson.SetBytes(stepStop, "index", st.ActiveStepIndex)
|
||||
out = append(out, translatorcommon.SSEEventData("step.stop", stepStop))
|
||||
st.ActiveStepOpen = false
|
||||
st.ActiveStepType = ""
|
||||
return out
|
||||
}
|
||||
|
||||
func buildCodexMessageItemToInteractions(item gjson.Result) []byte {
|
||||
var contents [][]byte
|
||||
item.Get("content").ForEach(func(_, content gjson.Result) bool {
|
||||
if contentItem := codexContentToInteractionsContent(content); len(contentItem) > 0 {
|
||||
contents = append(contents, contentItem)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(contents) == 0 {
|
||||
return nil
|
||||
}
|
||||
step := []byte(`{"type":"model_output","content":[]}`)
|
||||
return translatorcommon.SetRawArrayItems(step, "content", contents)
|
||||
}
|
||||
|
||||
func buildCodexReasoningItemToInteractions(item gjson.Result) []byte {
|
||||
text := codexReasoningText(item)
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
step := []byte(`{"type":"thought","content":[{"type":"text","text":""}]}`)
|
||||
step, _ = sjson.SetBytes(step, "content.0.text", text)
|
||||
return step
|
||||
}
|
||||
|
||||
func buildCodexFunctionCallItemToInteractions(item gjson.Result) []byte {
|
||||
step := []byte(`{"type":"function_call","name":"","arguments":{}}`)
|
||||
step, _ = sjson.SetBytes(step, "name", item.Get("name").String())
|
||||
if callID := codexItemCallID(item); callID != "" {
|
||||
step, _ = sjson.SetBytes(step, "call_id", callID)
|
||||
}
|
||||
if args := codexArgumentsJSON(item.Get("arguments")); len(args) > 0 {
|
||||
step, _ = sjson.SetRawBytes(step, "arguments", args)
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
func buildCodexImageItemToInteractions(item gjson.Result) []byte {
|
||||
result := item.Get("result").String()
|
||||
if result == "" {
|
||||
return nil
|
||||
}
|
||||
step := []byte(`{"type":"model_output","content":[{"type":"image","mime_type":"","data":""}]}`)
|
||||
step, _ = sjson.SetBytes(step, "content.0.mime_type", mimeTypeFromCodexOutputFormat(item.Get("output_format").String()))
|
||||
step, _ = sjson.SetBytes(step, "content.0.data", result)
|
||||
return step
|
||||
}
|
||||
|
||||
func appendCodexMessageItemToInteractions(out []byte, item gjson.Result) []byte {
|
||||
if step := buildCodexMessageItemToInteractions(item); len(step) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "steps.-1", step)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCodexReasoningItemToInteractions(out []byte, item gjson.Result) []byte {
|
||||
if step := buildCodexReasoningItemToInteractions(item); len(step) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "steps.-1", step)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCodexFunctionCallItemToInteractions(out []byte, item gjson.Result) []byte {
|
||||
if step := buildCodexFunctionCallItemToInteractions(item); len(step) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "steps.-1", step)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCodexImageItemToInteractions(out []byte, item gjson.Result) []byte {
|
||||
if step := buildCodexImageItemToInteractions(item); len(step) > 0 {
|
||||
out, _ = sjson.SetRawBytes(out, "steps.-1", step)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCodexMessageItemToInteractionsStream(out [][]byte, st *codexToInteractionsStreamState, item gjson.Result) [][]byte {
|
||||
item.Get("content").ForEach(func(_, content gjson.Result) bool {
|
||||
if text := codexContentText(content); text != "" {
|
||||
out = ensureCodexInteractionsStep(out, st, "model_output", item)
|
||||
delta := []byte(`{"index":0,"delta":{"text":"","type":"text"},"event_type":"step.delta"}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.text", text)
|
||||
out = append(out, translatorcommon.SSEEventData("step.delta", delta))
|
||||
}
|
||||
return true
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func appendCodexReasoningItemToInteractionsStream(out [][]byte, st *codexToInteractionsStreamState, item gjson.Result) [][]byte {
|
||||
text := codexReasoningText(item)
|
||||
if text == "" {
|
||||
return out
|
||||
}
|
||||
out = ensureCodexInteractionsStep(out, st, "thought", item)
|
||||
delta := []byte(`{"index":0,"delta":{"content":{"text":"","type":"text"},"type":"thought_summary"},"event_type":"step.delta"}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.content.text", text)
|
||||
return append(out, translatorcommon.SSEEventData("step.delta", delta))
|
||||
}
|
||||
|
||||
func appendCodexFunctionCallItemToInteractionsStream(out [][]byte, st *codexToInteractionsStreamState, item gjson.Result) [][]byte {
|
||||
out = ensureCodexInteractionsStep(out, st, "function_call", item)
|
||||
delta := []byte(`{"index":0,"delta":{"arguments":"","type":"arguments_delta"},"event_type":"step.delta"}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.arguments", item.Get("arguments").String())
|
||||
return append(out, translatorcommon.SSEEventData("step.delta", delta))
|
||||
}
|
||||
|
||||
func appendCodexImageItemToInteractionsStream(out [][]byte, st *codexToInteractionsStreamState, item gjson.Result) [][]byte {
|
||||
result := item.Get("result").String()
|
||||
if result == "" {
|
||||
return out
|
||||
}
|
||||
out = ensureCodexInteractionsStep(out, st, "model_output", item)
|
||||
delta := []byte(`{"index":0,"delta":{"content":{"type":"image","mime_type":"","data":""},"type":"content"},"event_type":"step.delta"}`)
|
||||
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
|
||||
delta, _ = sjson.SetBytes(delta, "delta.content.mime_type", mimeTypeFromCodexOutputFormat(item.Get("output_format").String()))
|
||||
delta, _ = sjson.SetBytes(delta, "delta.content.data", result)
|
||||
return append(out, translatorcommon.SSEEventData("step.delta", delta))
|
||||
}
|
||||
|
||||
func codexContentToInteractionsContent(content gjson.Result) []byte {
|
||||
if text := codexContentText(content); text != "" {
|
||||
item := []byte(`{"type":"text","text":""}`)
|
||||
item, _ = sjson.SetBytes(item, "text", text)
|
||||
return item
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexContentText(content gjson.Result) string {
|
||||
for _, path := range []string{"text", "content"} {
|
||||
if value := content.Get(path); value.Exists() && value.Type == gjson.String {
|
||||
return value.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func codexReasoningText(item gjson.Result) string {
|
||||
if content := item.Get("content"); content.Exists() {
|
||||
if content.Type == gjson.String {
|
||||
return content.String()
|
||||
}
|
||||
if content.IsArray() {
|
||||
var builder strings.Builder
|
||||
content.ForEach(func(_, part gjson.Result) bool {
|
||||
text := codexContentText(part)
|
||||
if text == "" {
|
||||
text = part.Get("summary_text").String()
|
||||
}
|
||||
if text == "" {
|
||||
return true
|
||||
}
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
builder.WriteString(text)
|
||||
return true
|
||||
})
|
||||
return builder.String()
|
||||
}
|
||||
}
|
||||
if summary := item.Get("summary"); summary.Exists() {
|
||||
if summary.Type == gjson.String {
|
||||
return summary.String()
|
||||
}
|
||||
if summary.IsArray() {
|
||||
var builder strings.Builder
|
||||
summary.ForEach(func(_, part gjson.Result) bool {
|
||||
text := codexContentText(part)
|
||||
if text == "" {
|
||||
return true
|
||||
}
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
builder.WriteString(text)
|
||||
return true
|
||||
})
|
||||
return builder.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func codexItemCallID(item gjson.Result) string {
|
||||
if callID := strings.TrimSpace(item.Get("call_id").String()); callID != "" {
|
||||
return callID
|
||||
}
|
||||
return strings.TrimSpace(item.Get("id").String())
|
||||
}
|
||||
|
||||
func codexArgumentsJSON(arguments gjson.Result) []byte {
|
||||
if !arguments.Exists() {
|
||||
return nil
|
||||
}
|
||||
if arguments.Type == gjson.String {
|
||||
parsed := gjson.Parse(arguments.String())
|
||||
if parsed.Exists() && parsed.IsObject() {
|
||||
return []byte(arguments.String())
|
||||
}
|
||||
return []byte(`{}`)
|
||||
}
|
||||
if arguments.IsObject() {
|
||||
return []byte(arguments.Raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCodexInteractionsUsage(out []byte, path string, usage gjson.Result, stream bool) []byte {
|
||||
if !usage.Exists() {
|
||||
return out
|
||||
}
|
||||
inputTokens := usage.Get("input_tokens").Int()
|
||||
outputTokens := usage.Get("output_tokens").Int()
|
||||
if inputTokens == 0 {
|
||||
inputTokens = usage.Get("prompt_tokens").Int()
|
||||
}
|
||||
if outputTokens == 0 {
|
||||
outputTokens = usage.Get("completion_tokens").Int()
|
||||
}
|
||||
totalTokens := usage.Get("total_tokens").Int()
|
||||
if totalTokens == 0 {
|
||||
totalTokens = inputTokens + outputTokens
|
||||
}
|
||||
reasoningTokens := usage.Get("output_tokens_details.reasoning_tokens").Int()
|
||||
if reasoningTokens == 0 {
|
||||
reasoningTokens = usage.Get("reasoning_tokens").Int()
|
||||
}
|
||||
cachedTokens := usage.Get("input_tokens_details.cached_tokens").Int()
|
||||
if cachedTokens == 0 {
|
||||
cachedTokens = usage.Get("cached_tokens").Int()
|
||||
}
|
||||
if stream {
|
||||
out, _ = sjson.SetBytes(out, path+".total_tokens", totalTokens)
|
||||
out, _ = sjson.SetBytes(out, path+".total_input_tokens", inputTokens)
|
||||
out, _ = sjson.SetRawBytes(out, path+".input_tokens_by_modality", []byte(fmt.Sprintf(`[{"modality":"text","tokens":%d}]`, inputTokens)))
|
||||
out, _ = sjson.SetBytes(out, path+".total_cached_tokens", cachedTokens)
|
||||
out, _ = sjson.SetBytes(out, path+".total_output_tokens", outputTokens)
|
||||
out, _ = sjson.SetBytes(out, path+".total_tool_use_tokens", 0)
|
||||
out, _ = sjson.SetBytes(out, path+".total_thought_tokens", reasoningTokens)
|
||||
return out
|
||||
}
|
||||
out, _ = sjson.SetBytes(out, path+".input_tokens", inputTokens)
|
||||
out, _ = sjson.SetBytes(out, path+".output_tokens", outputTokens)
|
||||
out, _ = sjson.SetBytes(out, path+".total_tokens", totalTokens)
|
||||
if reasoningTokens > 0 {
|
||||
out, _ = sjson.SetBytes(out, path+".reasoning_tokens", reasoningTokens)
|
||||
}
|
||||
if cachedTokens > 0 {
|
||||
out, _ = sjson.SetBytes(out, path+".cached_tokens", cachedTokens)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mimeTypeFromCodexOutputFormat(outputFormat string) string {
|
||||
if outputFormat == "" {
|
||||
return "image/png"
|
||||
}
|
||||
if strings.Contains(outputFormat, "/") {
|
||||
return outputFormat
|
||||
}
|
||||
switch strings.ToLower(outputFormat) {
|
||||
case "png":
|
||||
return "image/png"
|
||||
case "jpg", "jpeg":
|
||||
return "image/jpeg"
|
||||
case "webp":
|
||||
return "image/webp"
|
||||
case "gif":
|
||||
return "image/gif"
|
||||
default:
|
||||
return "image/png"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConvertInteractionsRequestToCodexWithToolMessagesDirect(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToCodex("codex-test", []byte(`{"model":"codex-test","system_instruction":"be brief","input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]},{"type":"thought","content":[{"type":"text","text":"thinking"}]},{"type":"function_call","name":"lookup","call_id":"call_1","arguments":{"q":"x"}},{"type":"function_result","name":"lookup","call_id":"call_1","result":{"ok":true}}],"tools":[{"type":"function","name":"lookup","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}]}`), false)
|
||||
if got := gjson.GetBytes(out, "instructions").String(); got != "be brief" {
|
||||
t.Fatalf("instructions = %q, want be brief. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.0.content.0.text").String(); got != "hi" {
|
||||
t.Fatalf("input.0.content.0.text = %q, want hi. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.1.type").String(); got != "reasoning" {
|
||||
t.Fatalf("input.1.type = %q, want reasoning. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.2.type").String(); got != "function_call" {
|
||||
t.Fatalf("input.2.type = %q, want function_call. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.2.call_id").String(); got != "call_1" {
|
||||
t.Fatalf("function_call call_id = %q, want call_1. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.3.type").String(); got != "function_call_output" {
|
||||
t.Fatalf("input.3.type = %q, want function_call_output. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "lookup" {
|
||||
t.Fatalf("tools.0.name = %q, want lookup. Output: %s", got, string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "contents").Exists() || gjson.GetBytes(out, "systemInstruction").Exists() {
|
||||
t.Fatalf("Codex request must not use foreign request shape. Output: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToCodexPreservesNonImageMediaContent(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToCodex("codex-test", []byte(`{"model":"codex-test","input":[{"type":"model_output","content":[{"type":"audio","mime_type":"audio/wav","data":"UklGRg=="},{"type":"video","mime_type":"video/mp4","data":"AAAAIGZ0eXA="},{"type":"document","mime_type":"application/pdf","data":"JVBERi0="}]}]}`), false)
|
||||
|
||||
if got := gjson.GetBytes(out, "input.0.role").String(); got != "assistant" {
|
||||
t.Fatalf("input.0.role = %q, want assistant. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.0.content.0.type").String(); got != "input_audio" {
|
||||
t.Fatalf("audio content type = %q, want input_audio. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.1.content.0.type").String(); got != "input_file" {
|
||||
t.Fatalf("video content type = %q, want input_file. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "input.2.content.0.type").String(); got != "input_file" {
|
||||
t.Fatalf("document content type = %q, want input_file. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToCodexPreservesTopLevelThinkingLevel(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToCodex("codex-test", []byte(`{"model":"codex-test","generation_config":{"thinking_level":"high"},"input":"hi"}`), true)
|
||||
if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "high" {
|
||||
t.Fatalf("reasoning.effort = %q, want high. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "stream").Bool(); !got {
|
||||
t.Fatalf("stream = %v, want true. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToCodexUsesBodyStream(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToCodex("codex-test", []byte(`{"model":"codex-test","stream":true,"input":"hi"}`), false)
|
||||
if got := gjson.GetBytes(out, "stream").Bool(); !got {
|
||||
t.Fatalf("stream = %v, want true. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInteractionsRequestToCodexFunctionDeclarations(t *testing.T) {
|
||||
out := ConvertInteractionsRequestToCodex("codex-test", []byte(`{"model":"codex-test","input":"hi","tools":[{"function_declarations":[{"name":"lookup","description":"Lookup data","parameters":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"q":{"type":"string"}}}}]}]}`), false)
|
||||
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "function" {
|
||||
t.Fatalf("tools.0.type = %q, want function. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "lookup" {
|
||||
t.Fatalf("tools.0.name = %q, want lookup. Output: %s", got, string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "tools.0.parameters.$schema").Exists() {
|
||||
t.Fatalf("tool parameters should not keep $schema. Output: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToInteractionsIncompleteTerminal(t *testing.T) {
|
||||
raw := []byte(`{"type":"response.incomplete","response":{"id":"resp_1","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`)
|
||||
nonStreamOut := ConvertCodexResponseToInteractionsNonStream(context.Background(), "codex-test", nil, nil, raw, nil)
|
||||
if got := gjson.GetBytes(nonStreamOut, "status").String(); got != "incomplete" {
|
||||
t.Fatalf("non-stream status = %q, want incomplete. Output: %s", got, nonStreamOut)
|
||||
}
|
||||
|
||||
var param any
|
||||
streamOut := ConvertCodexResponseToInteractions(context.Background(), "codex-test", nil, nil, append([]byte("data: "), raw...), ¶m)
|
||||
payload := findCodexInteractionsEventPayload(streamOut, "interaction.completed")
|
||||
if len(payload) == 0 {
|
||||
t.Fatalf("stream incomplete event did not terminate interaction: %q", streamOut)
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "interaction.status").String(); got != "incomplete" {
|
||||
t.Fatalf("stream status = %q, want incomplete. Payload: %s", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToInteractionsNonStream(t *testing.T) {
|
||||
raw := []byte(`{"type":"response.completed","response":{"id":"resp_1","created_at":1700000000,"usage":{"input_tokens":3,"output_tokens":2},"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]},{"type":"reasoning","content":"thinking"},{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\"q\":\"x\"}"}]}}`)
|
||||
out := ConvertCodexResponseToInteractionsNonStream(context.Background(), "codex-test", nil, nil, raw, nil)
|
||||
if got := gjson.GetBytes(out, "steps.0.content.0.text").String(); got != "ok" {
|
||||
t.Fatalf("steps.0.content.0.text = %q, want ok. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "steps.1.type").String(); got != "thought" {
|
||||
t.Fatalf("steps.1.type = %q, want thought. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "steps.2.type").String(); got != "function_call" {
|
||||
t.Fatalf("steps.2.type = %q, want function_call. Output: %s", got, string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "usage.total_tokens").Int(); got != 5 {
|
||||
t.Fatalf("usage.total_tokens = %d, want 5. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToInteractionsStream(t *testing.T) {
|
||||
var param any
|
||||
events := ConvertCodexResponseToInteractions(context.Background(), "codex-test", nil, nil, []byte(`data: {"type":"response.output_text.delta","delta":"ok"}`), ¶m)
|
||||
payload := findCodexInteractionsEventPayload(events, "step.delta")
|
||||
if len(payload) == 0 {
|
||||
t.Fatalf("step.delta event not found: %q", events)
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "delta.text").String(); got != "ok" {
|
||||
t.Fatalf("delta.text = %q, want ok. Payload: %s", got, string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToInteractionsStreamFunctionCallStartHasCallID(t *testing.T) {
|
||||
var param any
|
||||
events := ConvertCodexResponseToInteractions(context.Background(), "codex-test", nil, nil, []byte(`data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\"q\":\"x\"}"}}`), ¶m)
|
||||
payload := findCodexInteractionsEventPayload(events, "step.start")
|
||||
if got := gjson.GetBytes(payload, "step.call_id").String(); got != "call_1" {
|
||||
t.Fatalf("step.call_id = %q, want call_1. Payload: %s", got, string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToInteractionsStreamCompletesAfterSteps(t *testing.T) {
|
||||
var param any
|
||||
var events [][]byte
|
||||
for _, chunk := range [][]byte{
|
||||
[]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"codex-test"}}`),
|
||||
[]byte(`data: {"type":"response.output_text.delta","delta":"我将调用工具。"}`),
|
||||
[]byte(`data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\"q\":\"weather\"}"},"output_index":1}`),
|
||||
[]byte(`data: {"type":"response.completed","response":{"id":"resp_1","output":[],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`),
|
||||
} {
|
||||
events = append(events, ConvertCodexResponseToInteractions(context.Background(), "codex-test", nil, nil, chunk, ¶m)...)
|
||||
}
|
||||
|
||||
got := strings.Join(codexInteractionsEventNames(events), ",")
|
||||
want := "interaction.created,interaction.status_update,step.start,step.delta,step.stop,step.start,step.delta,step.stop,interaction.completed,done"
|
||||
if got != want {
|
||||
t.Fatalf("events = %s, want %s", got, want)
|
||||
}
|
||||
completed := findCodexInteractionsEventPayload(events, "interaction.completed")
|
||||
if gotTokens := gjson.GetBytes(completed, "interaction.usage.total_tokens").Int(); gotTokens != 3 {
|
||||
t.Fatalf("total_tokens = %d, want 3. Payload: %s", gotTokens, string(completed))
|
||||
}
|
||||
}
|
||||
|
||||
func findCodexInteractionsEventPayload(events [][]byte, eventType string) []byte {
|
||||
prefix := []byte("data:")
|
||||
for _, event := range events {
|
||||
eventName := codexInteractionsFrameEventName(event)
|
||||
for _, line := range bytes.Split(event, []byte("\n")) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if !bytes.HasPrefix(line, prefix) {
|
||||
continue
|
||||
}
|
||||
payload := bytes.TrimSpace(line[len(prefix):])
|
||||
if codexInteractionsEventName(eventName, payload) == eventType {
|
||||
return payload
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexInteractionsEventNames(events [][]byte) []string {
|
||||
names := make([]string, 0, len(events))
|
||||
for _, event := range events {
|
||||
eventName := codexInteractionsFrameEventName(event)
|
||||
for _, line := range bytes.Split(event, []byte("\n")) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if !bytes.HasPrefix(line, []byte("data:")) {
|
||||
continue
|
||||
}
|
||||
payload := bytes.TrimSpace(line[len("data:"):])
|
||||
if name := codexInteractionsEventName(eventName, payload); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func codexInteractionsEventName(eventName string, payload []byte) string {
|
||||
if eventType := gjson.GetBytes(payload, "event_type").String(); eventType != "" {
|
||||
return eventType
|
||||
}
|
||||
if eventType := gjson.GetBytes(payload, "type").String(); eventType != "" {
|
||||
return eventType
|
||||
}
|
||||
return eventName
|
||||
}
|
||||
|
||||
func codexInteractionsFrameEventName(event []byte) string {
|
||||
for _, line := range bytes.Split(event, []byte("\n")) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if bytes.HasPrefix(line, []byte("event:")) {
|
||||
return strings.TrimSpace(string(line[len("event:"):]))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package interactions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestCleanedCodexToolParametersPreservesCanonicalSchema(t *testing.T) {
|
||||
input := []byte(`{"type":"object","properties":{"value":{"type":"string"}},"additionalProperties":false}`)
|
||||
|
||||
output := []byte(cleanedCodexToolParameters(gjson.ParseBytes(input)))
|
||||
|
||||
if string(output) != string(input) {
|
||||
t.Fatalf("canonical schema changed:\n got: %s\nwant: %s", output, input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetInteractionsCodexRawIfDifferentReusesMatchingValue(t *testing.T) {
|
||||
input := []byte(`{"tool_choice":"auto","input":[]}`)
|
||||
value := gjson.Parse(`"auto"`)
|
||||
|
||||
output := setInteractionsCodexRawIfDifferent(input, "tool_choice", value)
|
||||
|
||||
if &output[0] != &input[0] {
|
||||
t.Fatal("matching raw value caused a payload copy")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue