fix(translator): improve tool response handling for non-string content

- Added `setToolCallOutputContent` to process various content types, including arrays and fallback cases.
- Implemented robust handling for specific tool output types like text, image URLs, and files, ensuring proper serialization.
- Improved fallback logic to handle unexpected or missing data.

Fixed: #2313
Closes: #2349
This commit is contained in:
Luis Pater
2026-05-04 05:50:01 +08:00
parent 38dad2afdf
commit 17be6442a8
2 changed files with 263 additions and 2 deletions
@@ -121,13 +121,13 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
case "tool":
// Handle tool response messages as top-level function_call_output objects
toolCallID := m.Get("tool_call_id").String()
content := m.Get("content").String()
content := m.Get("content")
// Create function_call_output object
funcOutput := []byte(`{}`)
funcOutput, _ = sjson.SetBytes(funcOutput, "type", "function_call_output")
funcOutput, _ = sjson.SetBytes(funcOutput, "call_id", toolCallID)
funcOutput, _ = sjson.SetBytes(funcOutput, "output", content)
funcOutput = setToolCallOutputContent(funcOutput, content)
out, _ = sjson.SetRawBytes(out, "input.-1", funcOutput)
default:
@@ -359,6 +359,91 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
return out
}
func setToolCallOutputContent(funcOutput []byte, content gjson.Result) []byte {
switch {
case content.Type == gjson.String:
funcOutput, _ = sjson.SetBytes(funcOutput, "output", content.String())
case content.IsArray():
output := []byte(`[]`)
for _, item := range content.Array() {
output = appendToolOutputContentPart(output, item)
}
funcOutput, _ = sjson.SetRawBytes(funcOutput, "output", output)
default:
fallbackOutput := content.Raw
if fallbackOutput == "" {
fallbackOutput = content.String()
}
funcOutput, _ = sjson.SetBytes(funcOutput, "output", fallbackOutput)
}
return funcOutput
}
func appendToolOutputContentPart(output []byte, item gjson.Result) []byte {
switch item.Get("type").String() {
case "text":
part := []byte(`{}`)
part, _ = sjson.SetBytes(part, "type", "input_text")
part, _ = sjson.SetBytes(part, "text", item.Get("text").String())
output, _ = sjson.SetRawBytes(output, "-1", part)
case "image_url":
imageURL := item.Get("image_url.url").String()
fileID := item.Get("image_url.file_id").String()
if imageURL == "" && fileID == "" {
return appendToolOutputFallbackPart(output, item)
}
part := []byte(`{}`)
part, _ = sjson.SetBytes(part, "type", "input_image")
if imageURL != "" {
part, _ = sjson.SetBytes(part, "image_url", imageURL)
}
if fileID != "" {
part, _ = sjson.SetBytes(part, "file_id", fileID)
}
if detail := item.Get("image_url.detail").String(); detail != "" {
part, _ = sjson.SetBytes(part, "detail", detail)
}
output, _ = sjson.SetRawBytes(output, "-1", part)
case "file":
fileID := item.Get("file.file_id").String()
fileData := item.Get("file.file_data").String()
fileURL := item.Get("file.file_url").String()
if fileID == "" && fileData == "" && fileURL == "" {
return appendToolOutputFallbackPart(output, item)
}
part := []byte(`{}`)
part, _ = sjson.SetBytes(part, "type", "input_file")
if fileID != "" {
part, _ = sjson.SetBytes(part, "file_id", fileID)
}
if fileData != "" {
part, _ = sjson.SetBytes(part, "file_data", fileData)
}
if fileURL != "" {
part, _ = sjson.SetBytes(part, "file_url", fileURL)
}
if filename := item.Get("file.filename").String(); filename != "" {
part, _ = sjson.SetBytes(part, "filename", filename)
}
output, _ = sjson.SetRawBytes(output, "-1", part)
default:
output = appendToolOutputFallbackPart(output, item)
}
return output
}
func appendToolOutputFallbackPart(output []byte, item gjson.Result) []byte {
text := item.Raw
if text == "" {
text = item.String()
}
part := []byte(`{}`)
part, _ = sjson.SetBytes(part, "type", "input_text")
part, _ = sjson.SetBytes(part, "text", text)
output, _ = sjson.SetRawBytes(output, "-1", part)
return output
}
// shortenNameIfNeeded applies the simple shortening rule for a single name.
// If the name length exceeds 64, it will try to preserve the "mcp__" prefix and last segment.
// Otherwise it truncates to 64 characters.