fix(openai): avoid null content when converting chat-completions to responses
When a chat-completions message has no usable content parts (empty array, empty text part, or filtered-out image part), marshalChatInputContent marshalled a nil slice to JSON null. The upstream Responses API rejects a null content field with HTTP 400. Fall back to an empty string instead. Fixes #2515
This commit is contained in:
parent
6e66edbb09
commit
df82a3bc69
@ -225,6 +225,41 @@ func TestChatCompletionsToResponses_WhitespaceOnlyBase64ImageURLSkipped(t *testi
|
|||||||
assert.Equal(t, "Describe this", parts[0].Text)
|
assert.Equal(t, "Describe this", parts[0].Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChatCompletionsToResponses_EmptyContentNeverNull(t *testing.T) {
|
||||||
|
// Regression for #2515: the upstream Responses API rejects an input item
|
||||||
|
// whose content field is JSON null. Any chat-completions message that
|
||||||
|
// yields no usable content parts must serialize content as a string.
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
content json.RawMessage
|
||||||
|
}{
|
||||||
|
{"null content", json.RawMessage(`null`)},
|
||||||
|
{"empty array content", json.RawMessage(`[]`)},
|
||||||
|
{"only empty text part", json.RawMessage(`[{"type":"text","text":""}]`)},
|
||||||
|
{"only empty base64 image part", json.RawMessage(`[{"type":"image_url","image_url":{"url":"data:image/png;base64,"}}]`)},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req := &ChatCompletionsRequest{
|
||||||
|
Model: "gpt-5.5",
|
||||||
|
Messages: []ChatMessage{
|
||||||
|
{Role: "user", Content: tc.content},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := ChatCompletionsToResponses(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, string(resp.Input), `"content":null`,
|
||||||
|
"converted input must not contain a null content field")
|
||||||
|
|
||||||
|
var items []ResponsesInputItem
|
||||||
|
require.NoError(t, json.Unmarshal(resp.Input, &items))
|
||||||
|
require.Len(t, items, 1)
|
||||||
|
assert.Equal(t, `""`, string(items[0].Content),
|
||||||
|
"content must be an empty string, not null")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestChatCompletionsToResponses_SystemArrayContent(t *testing.T) {
|
func TestChatCompletionsToResponses_SystemArrayContent(t *testing.T) {
|
||||||
req := &ChatCompletionsRequest{
|
req := &ChatCompletionsRequest{
|
||||||
Model: "gpt-4o",
|
Model: "gpt-4o",
|
||||||
|
|||||||
@ -325,7 +325,14 @@ func marshalChatInputContent(content chatMessageContent) (json.RawMessage, error
|
|||||||
if content.Text != nil {
|
if content.Text != nil {
|
||||||
return json.Marshal(*content.Text)
|
return json.Marshal(*content.Text)
|
||||||
}
|
}
|
||||||
return json.Marshal(convertChatContentPartsToResponses(content.Parts))
|
parts := convertChatContentPartsToResponses(content.Parts)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
// A nil slice marshals to JSON null, which the upstream Responses API
|
||||||
|
// rejects ("expected an array of objects or string, but got null").
|
||||||
|
// Fall back to an empty string when no usable parts remain.
|
||||||
|
return json.Marshal("")
|
||||||
|
}
|
||||||
|
return json.Marshal(parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertChatContentPartsToResponses(parts []ChatContentPart) []ResponsesContentPart {
|
func convertChatContentPartsToResponses(parts []ChatContentPart) []ResponsesContentPart {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user