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:
wucm667 2026-05-17 11:20:05 +08:00
parent 6e66edbb09
commit df82a3bc69
2 changed files with 43 additions and 1 deletions

View File

@ -225,6 +225,41 @@ func TestChatCompletionsToResponses_WhitespaceOnlyBase64ImageURLSkipped(t *testi
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) {
req := &ChatCompletionsRequest{
Model: "gpt-4o",

View File

@ -325,7 +325,14 @@ func marshalChatInputContent(content chatMessageContent) (json.RawMessage, error
if content.Text != nil {
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 {