-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
fix(codex): preserve image content in tool outputs #2349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
38cf661
37cfef3
bcd8672
726381c
831e937
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,13 +121,94 @@ 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) | ||
|
|
||
| // Handle content: can be string, array, or any other JSON value. | ||
| // Always set output to avoid dropping tool payloads for null/non-array content. | ||
| if content.Type == gjson.String { | ||
| funcOutput, _ = sjson.SetBytes(funcOutput, "output", content.String()) | ||
| } else if content.IsArray() { | ||
| // Build output array from content items | ||
| outputArr := []byte(`[]`) | ||
| items := content.Array() | ||
| for j := 0; j < len(items); j++ { | ||
| it := items[j] | ||
| t := it.Get("type").String() | ||
| switch t { | ||
| case "text": | ||
| outputPart := []byte(`{}`) | ||
| outputPart, _ = sjson.SetBytes(outputPart, "type", "input_text") | ||
| outputPart, _ = sjson.SetBytes(outputPart, "text", it.Get("text").String()) | ||
| outputArr, _ = sjson.SetRawBytes(outputArr, "-1", outputPart) | ||
| case "image_url": | ||
| // Handle image_url content in tool message | ||
| imageURL := it.Get("image_url.url").String() | ||
| fileID := it.Get("image_url.file_id").String() | ||
| if imageURL != "" || fileID != "" { | ||
| outputPart := []byte(`{}`) | ||
| outputPart, _ = sjson.SetBytes(outputPart, "type", "input_image") | ||
| if imageURL != "" { | ||
| outputPart, _ = sjson.SetBytes(outputPart, "image_url", imageURL) | ||
| } | ||
| if fileID != "" { | ||
| outputPart, _ = sjson.SetBytes(outputPart, "file_id", fileID) | ||
| } | ||
| if detail := it.Get("image_url.detail").String(); detail != "" { | ||
| outputPart, _ = sjson.SetBytes(outputPart, "detail", detail) | ||
| } | ||
| outputArr, _ = sjson.SetRawBytes(outputArr, "-1", outputPart) | ||
| } else { | ||
| outputPart := []byte(`{}`) | ||
| outputPart, _ = sjson.SetBytes(outputPart, "type", "input_text") | ||
| outputPart, _ = sjson.SetBytes(outputPart, "text", it.Raw) | ||
| outputArr, _ = sjson.SetRawBytes(outputArr, "-1", outputPart) | ||
| } | ||
| case "file": | ||
| // Handle file content in tool message | ||
| fileID := it.Get("file.file_id").String() | ||
| fileData := it.Get("file.file_data").String() | ||
| filename := it.Get("file.filename").String() | ||
| fileUrl := it.Get("file.file_url").String() | ||
| if fileID != "" || fileData != "" || filename != "" || fileUrl != "" { | ||
| outputPart := []byte(`{}`) | ||
| outputPart, _ = sjson.SetBytes(outputPart, "type", "input_file") | ||
|
Comment on lines
+177
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This guard now allows Useful? React with 👍 / 👎. |
||
| if fileID != "" { | ||
| outputPart, _ = sjson.SetBytes(outputPart, "file_id", fileID) | ||
| } | ||
| if fileData != "" { | ||
| outputPart, _ = sjson.SetBytes(outputPart, "file_data", fileData) | ||
| } | ||
| if filename != "" { | ||
| outputPart, _ = sjson.SetBytes(outputPart, "filename", filename) | ||
| } | ||
| if fileUrl != "" { | ||
| outputPart, _ = sjson.SetBytes(outputPart, "file_url", fileUrl) | ||
| } | ||
| outputArr, _ = sjson.SetRawBytes(outputArr, "-1", outputPart) | ||
| } | ||
|
|
||
| default: | ||
| // Fallback: convert unknown types to string representation | ||
| outputPart := []byte(`{}`) | ||
| outputPart, _ = sjson.SetBytes(outputPart, "type", "input_text") | ||
| outputPart, _ = sjson.SetBytes(outputPart, "text", it.Raw) | ||
| outputArr, _ = sjson.SetRawBytes(outputArr, "-1", outputPart) | ||
| } | ||
| } | ||
| funcOutput, _ = sjson.SetRawBytes(funcOutput, "output", outputArr) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This branch always rewrites array-based tool content into Useful? React with 👍 / 👎. |
||
| } else { | ||
| fallbackOutput := content.Raw | ||
| if fallbackOutput == "" { | ||
| fallbackOutput = content.String() | ||
| } | ||
| funcOutput, _ = sjson.SetBytes(funcOutput, "output", fallbackOutput) | ||
| } | ||
|
|
||
| out, _ = sjson.SetRawBytes(out, "input.-1", funcOutput) | ||
|
Comment on lines
128
to
212
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Throughout this It is highly recommended to implement error handling for these operations, at least by logging the errors. For example, you could change |
||
|
|
||
| default: | ||
|
|
@@ -177,16 +258,33 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b | |
| if u := it.Get("image_url.url"); u.Exists() { | ||
| part, _ = sjson.SetBytes(part, "image_url", u.String()) | ||
| } | ||
| if fid := it.Get("image_url.file_id").String(); fid != "" { | ||
| part, _ = sjson.SetBytes(part, "file_id", fid) | ||
| } | ||
| if detail := it.Get("image_url.detail").String(); detail != "" { | ||
| part, _ = sjson.SetBytes(part, "detail", detail) | ||
| } | ||
| msg, _ = sjson.SetRawBytes(msg, "content.-1", part) | ||
| } | ||
| case "file": | ||
| if role == "user" { | ||
| fileID := it.Get("file.file_id").String() | ||
| fileData := it.Get("file.file_data").String() | ||
| fileURL := it.Get("file.file_url").String() | ||
| filename := it.Get("file.filename").String() | ||
| if fileData != "" { | ||
|
|
||
| if fileData != "" || fileURL != "" { | ||
| part := []byte(`{}`) | ||
| part, _ = sjson.SetBytes(part, "type", "input_file") | ||
| part, _ = sjson.SetBytes(part, "file_data", fileData) | ||
| 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 != "" { | ||
| part, _ = sjson.SetBytes(part, "filename", filename) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch now sets
function_call_output.outputonly when toolcontentis a string or an array. If a caller sends tool content as an object, number, boolean, ornull, the generatedfunction_call_outputhas nooutputfield at all, whereas the previous code always serializedcontentwith.String(). In those inputs, tool results are dropped (or the request is rejected as malformed), so this should include anelsefallback that preserves non-array payloads.Useful? React with 👍 / 👎.