diff --git a/dto/openai_request.go b/dto/openai_request.go index 8c104ddd242d..44cb5c9a57be 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -1,6 +1,7 @@ package dto import ( + "bytes" "encoding/json" "fmt" "strings" @@ -947,6 +948,58 @@ func (r *OpenAIResponsesRequest) GetToolsMap() []map[string]any { return toolsMap } +// NormalizeInputNullContent rewrites Responses API message input items whose +// `content` is explicitly null into an empty string. Some OpenAI-compatible +// chat clients emit empty assistant messages as null, while Responses-compatible +// upstreams reject null content. Non-message input items such as reasoning and +// function_call use different schemas and must be preserved. +func (r *OpenAIResponsesRequest) NormalizeInputNullContent() error { + if r == nil || len(r.Input) == 0 { + return nil + } + if common.GetJsonType(r.Input) != "array" { + return nil + } + + var items []any + if err := common.Unmarshal(r.Input, &items); err != nil { + return err + } + + changed := false + for _, rawItem := range items { + item, ok := rawItem.(map[string]any) + if !ok || item == nil { + continue + } + itemType := strings.TrimSpace(common.Interface2String(item["type"])) + role := strings.TrimSpace(common.Interface2String(item["role"])) + if itemType != "" && itemType != "message" { + continue + } + if itemType == "" && role == "" { + continue + } + content, exists := item["content"] + if !exists || content != nil { + continue + } + item["content"] = "" + changed = true + } + + if !changed { + return nil + } + + normalized, err := common.Marshal(items) + if err != nil { + return err + } + r.Input = json.RawMessage(bytes.TrimSpace(normalized)) + return nil +} + type Reasoning struct { Effort string `json:"effort,omitempty"` Summary string `json:"summary,omitempty"` diff --git a/dto/openai_request_zero_value_test.go b/dto/openai_request_zero_value_test.go index 4b0dbd7c25ee..154f882daaeb 100644 --- a/dto/openai_request_zero_value_test.go +++ b/dto/openai_request_zero_value_test.go @@ -71,3 +71,27 @@ func TestOpenAIResponsesRequestPreserveExplicitZeroValues(t *testing.T) { require.True(t, gjson.GetBytes(encoded, "stream").Exists()) require.True(t, gjson.GetBytes(encoded, "top_p").Exists()) } + +func TestOpenAIResponsesRequestNormalizeInputNullContent(t *testing.T) { + raw := []byte(`{ + "model":"gpt-5.4", + "input":[ + {"role":"user","content":"hello"}, + {"role":"assistant","content":null}, + {"type":"function_call","call_id":"call_1","name":"demo","arguments":"{}"}, + {"type":"reasoning","summary":[],"content":null,"encrypted_content":"secret"} + ] + }`) + + var req OpenAIResponsesRequest + err := common.Unmarshal(raw, &req) + require.NoError(t, err) + + err = req.NormalizeInputNullContent() + require.NoError(t, err) + + require.Equal(t, "\"\"", gjson.GetBytes(req.Input, "1.content").Raw) + require.Equal(t, "", gjson.GetBytes(req.Input, "1.content").String()) + require.Equal(t, "hello", gjson.GetBytes(req.Input, "0.content").String()) + require.Equal(t, "null", gjson.GetBytes(req.Input, "3.content").Raw) +} diff --git a/relay/responses_handler.go b/relay/responses_handler.go index 58324aa7cec9..43c3c54ff375 100644 --- a/relay/responses_handler.go +++ b/relay/responses_handler.go @@ -59,6 +59,9 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * if err != nil { return types.NewError(fmt.Errorf("failed to copy request to GeneralOpenAIRequest: %w", err), types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry()) } + if err := request.NormalizeInputNullContent(); err != nil { + return types.NewError(err, types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry()) + } err = helper.ModelMappedHelper(c, info, request) if err != nil { diff --git a/service/openaicompat/chat_to_responses.go b/service/openaicompat/chat_to_responses.go index 16096b88f597..1015f9471ba5 100644 --- a/service/openaicompat/chat_to_responses.go +++ b/service/openaicompat/chat_to_responses.go @@ -398,5 +398,9 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d } } + if err := out.NormalizeInputNullContent(); err != nil { + return nil, err + } + return out, nil }