From 8e292c8b8efd0fd8b75ce0bbeb9203e8b0ede2da Mon Sep 17 00:00:00 2001 From: iwehf Date: Wed, 31 Jan 2024 20:35:33 +0800 Subject: [PATCH 1/5] add seperate get_result and upload_result api for gpt task --- api/v1/inference_tasks/get_result.go | 88 ++++++-- api/v1/inference_tasks/get_result_test.go | 216 ++++++++++++------- api/v1/inference_tasks/gpt_response.go | 39 ++++ api/v1/inference_tasks/upload_result.go | 133 ++++++++++-- api/v1/inference_tasks/upload_result_test.go | 132 +++++++++--- api/v1/routes.go | 23 +- blockchain/task.go | 5 +- tests/gpt_resp.go | 37 ++-- tests/init.go | 2 +- 9 files changed, 506 insertions(+), 169 deletions(-) create mode 100644 api/v1/inference_tasks/gpt_response.go diff --git a/api/v1/inference_tasks/get_result.go b/api/v1/inference_tasks/get_result.go index 9c0e483..c30ede0 100644 --- a/api/v1/inference_tasks/get_result.go +++ b/api/v1/inference_tasks/get_result.go @@ -4,6 +4,7 @@ import ( "crynux_relay/api/v1/response" "crynux_relay/config" "crynux_relay/models" + "encoding/json" "errors" "os" "path/filepath" @@ -13,20 +14,20 @@ import ( "gorm.io/gorm" ) -type GetResultInput struct { +type GetSDResultInput struct { ImageNum string `path:"image_num" json:"image_num" description:"Image number" validate:"required"` TaskId uint64 `path:"task_id" json:"task_id" description:"Task id" validate:"required"` } -type GetResultInputWithSignature struct { - GetResultInput +type GetSDResultInputWithSignature struct { + GetSDResultInput Timestamp int64 `query:"timestamp" description:"Signature timestamp" validate:"required"` Signature string `query:"signature" description:"Signature" validate:"required"` } -func GetResult(ctx *gin.Context, in *GetResultInputWithSignature) error { +func GetSDResult(ctx *gin.Context, in *GetSDResultInputWithSignature) error { - match, address, err := ValidateSignature(in.GetResultInput, in.Timestamp, in.Signature) + match, address, err := ValidateSignature(in.GetSDResultInput, in.Timestamp, in.Signature) if err != nil || !match { @@ -57,18 +58,11 @@ func GetResult(ctx *gin.Context, in *GetResultInputWithSignature) error { appConfig := config.GetConfig() - var fileExt string - if task.TaskType == models.TaskTypeSD { - fileExt = ".png" - } else { - fileExt = ".json" - } - resultFile := filepath.Join( appConfig.DataDir.InferenceTasks, task.GetTaskIdAsString(), "results", - in.ImageNum+fileExt, + in.ImageNum+".png", ) if _, err := os.Stat(resultFile); err != nil { @@ -77,9 +71,75 @@ func GetResult(ctx *gin.Context, in *GetResultInputWithSignature) error { ctx.Header("Content-Description", "File Transfer") ctx.Header("Content-Transfer-Encoding", "binary") - ctx.Header("Content-Disposition", "attachment; filename="+in.ImageNum+fileExt) + ctx.Header("Content-Disposition", "attachment; filename="+in.ImageNum+".png") ctx.Header("Content-Type", "application/octet-stream") ctx.File(resultFile) return nil } + +type GetGPTResultInput struct { + TaskId uint64 `path:"task_id" json:"task_id" description:"Task id" validate:"required"` +} + +type GetGPTResultInputWithSignature struct { + GetGPTResultInput + Timestamp int64 `query:"timestamp" description:"Signature timestamp" validate:"required"` + Signature string `query:"signature" description:"Signature" validate:"required"` +} + +func GetGPTResult(ctx *gin.Context, in *GetGPTResultInputWithSignature) (*GPTTaskResponse, error) { + match, address, err := ValidateSignature(in.GetGPTResultInput, in.Timestamp, in.Signature) + + if err != nil || !match { + + if err != nil { + log.Debugln(err) + } + + return nil, response.NewValidationErrorResponse("signature", "Invalid signature") + } + + var task models.InferenceTask + + if err := config.GetDB().Where(&models.InferenceTask{TaskId: in.TaskId}).First(&task).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, response.NewValidationErrorResponse("task_id", "Task not found") + } else { + return nil, response.NewExceptionResponse(err) + } + } + + if task.Creator != address { + return nil, response.NewValidationErrorResponse("signature", "Signer not allowed") + } + + if task.Status != models.InferenceTaskResultsUploaded { + return nil, response.NewValidationErrorResponse("task_id", "Task results not uploaded") + } + + appConfig := config.GetConfig() + + resultFile := filepath.Join( + appConfig.DataDir.InferenceTasks, + task.GetTaskIdAsString(), + "results", + "0.json", + ) + + if _, err := os.Stat(resultFile); err != nil { + return nil, response.NewValidationErrorResponse("image_num", "File not found") + } + + resultContent, err := os.ReadFile(resultFile) + if err != nil { + return nil, response.NewExceptionResponse(err) + } + + result := &GPTTaskResponse{} + if err := json.Unmarshal(resultContent, result); err != nil { + return nil, response.NewExceptionResponse(err) + } + + return result, nil +} diff --git a/api/v1/inference_tasks/get_result_test.go b/api/v1/inference_tasks/get_result_test.go index 0ff92e9..be7a049 100644 --- a/api/v1/inference_tasks/get_result_test.go +++ b/api/v1/inference_tasks/get_result_test.go @@ -6,6 +6,7 @@ import ( "crynux_relay/models" "crynux_relay/tests" v1 "crynux_relay/tests/api/v1" + "encoding/json" "io" "net/http" "net/http/httptest" @@ -18,105 +19,161 @@ import ( ) func TestUnauthorizedGetImage(t *testing.T) { - for _, taskType := range tests.TaskTypes { - addresses, privateKeys, err := tests.PrepareAccounts() - assert.Equal(t, nil, err, "prepare accounts error") + addresses, privateKeys, err := tests.PrepareAccounts() + assert.Equal(t, nil, err, "prepare accounts error") - _, task, err := tests.PrepareResultUploadedTask(taskType, addresses, config.GetDB()) - assert.Equal(t, nil, err, "prepare task error") + _, task, err := tests.PrepareResultUploadedTask(models.TaskTypeSD, addresses, config.GetDB()) + assert.Equal(t, nil, err, "prepare task error") - getResultInput := &inference_tasks.GetResultInput{ - TaskId: task.TaskId, - ImageNum: "0", - } + getResultInput := &inference_tasks.GetSDResultInput{ + TaskId: task.TaskId, + ImageNum: "0", + } - timestamp, signature, err := v1.SignData(getResultInput, privateKeys[1]) - assert.Equal(t, nil, err, "sign data error") + timestamp, signature, err := v1.SignData(getResultInput, privateKeys[1]) + assert.Equal(t, nil, err, "sign data error") - r := callGetImageApi( - task.GetTaskIdAsString(), - "0", - timestamp, - signature) + r := callGetImageApi( + task.GetTaskIdAsString(), + "0", + timestamp, + signature) - v1.AssertValidationErrorResponse(t, r, "signature", "Signer not allowed") + v1.AssertValidationErrorResponse(t, r, "signature", "Signer not allowed") - t.Cleanup(func() { - tests.ClearDB() - if err := tests.ClearDataFolders(); err != nil { - t.Error(err) - } - }) - } + t.Cleanup(func() { + tests.ClearDB() + if err := tests.ClearDataFolders(); err != nil { + t.Error(err) + } + }) } -func TestGetImage(t *testing.T) { - for _, taskType := range tests.TaskTypes { - addresses, privateKeys, err := tests.PrepareAccounts() - assert.Equal(t, nil, err, "prepare accounts error") +func TestUnauthorizedGetGPTResponse(t *testing.T) { + addresses, privateKeys, err := tests.PrepareAccounts() + assert.Equal(t, nil, err, "prepare accounts error") - _, task, err := tests.PrepareResultUploadedTask(taskType, addresses, config.GetDB()) - assert.Equal(t, nil, err, "prepare task error") + _, task, err := tests.PrepareResultUploadedTask(models.TaskTypeLLM, addresses, config.GetDB()) + assert.Equal(t, nil, err, "prepare task error") - var imageNum, srcFile, dstFile string + getResultInput := &inference_tasks.GetGPTResultInput{ + TaskId: task.TaskId, + } - if taskType == models.TaskTypeSD { - imageNum = "2" - srcFile = "2.png" - dstFile = "downloaded.png" - } else { - imageNum = "0" - srcFile = "0.json" - dstFile = "downloaded.json" + timestamp, signature, err := v1.SignData(getResultInput, privateKeys[1]) + assert.Equal(t, nil, err, "sign data error") + + r := callGetGPTResponseApi( + task.GetTaskIdAsString(), + timestamp, + signature) + + v1.AssertValidationErrorResponse(t, r, "signature", "Signer not allowed") + + t.Cleanup(func() { + tests.ClearDB() + if err := tests.ClearDataFolders(); err != nil { + t.Error(err) } + }) +} - getResultInput := &inference_tasks.GetResultInput{ - TaskId: task.TaskId, - ImageNum: imageNum, +func TestGetImage(t *testing.T) { + t.Cleanup(func() { + tests.ClearDB() + if err := tests.ClearDataFolders(); err != nil { + t.Error(err) } + }) + + addresses, privateKeys, err := tests.PrepareAccounts() + assert.Equal(t, nil, err, "prepare accounts error") + + _, task, err := tests.PrepareResultUploadedTask(models.TaskTypeSD, addresses, config.GetDB()) + assert.Equal(t, nil, err, "prepare task error") + + imageNum := "2" + srcFile := "2.png" + dstFile := "downloaded.png" + + getResultInput := &inference_tasks.GetSDResultInput{ + TaskId: task.TaskId, + ImageNum: imageNum, + } + + timestamp, signature, err := v1.SignData(getResultInput, privateKeys[0]) + assert.Equal(t, nil, err, "sign data error") + + r := callGetImageApi( + task.GetTaskIdAsString(), + imageNum, + timestamp, + signature) + + assert.Equal(t, 200, r.Code, "wrong http status code. message: "+r.Body.String()) - timestamp, signature, err := v1.SignData(getResultInput, privateKeys[0]) - assert.Equal(t, nil, err, "sign data error") + appConfig := config.GetConfig() + imageFolder := filepath.Join( + appConfig.DataDir.InferenceTasks, + task.GetTaskIdAsString(), + "results", + ) - r := callGetImageApi( - task.GetTaskIdAsString(), - imageNum, - timestamp, - signature) + out, err := os.Create(filepath.Join(imageFolder, dstFile)) + assert.Equal(t, nil, err, "create tmp file error") - assert.Equal(t, 200, r.Code, "wrong http status code. message: "+string(r.Body.Bytes())) + _, err = io.Copy(out, r.Body) + assert.Equal(t, nil, err, "write tmp file error") - appConfig := config.GetConfig() - imageFolder := filepath.Join( - appConfig.DataDir.InferenceTasks, - task.GetTaskIdAsString(), - "results", - ) + err = out.Close() + assert.Equal(t, nil, err, "close tmp file error") - out, err := os.Create(filepath.Join(imageFolder, dstFile)) - assert.Equal(t, nil, err, "create tmp file error") + originalFile, err := os.Stat(filepath.Join(imageFolder, srcFile)) + assert.Equal(t, nil, err, "read original file error") - _, err = io.Copy(out, r.Body) - assert.Equal(t, nil, err, "write tmp file error") + downloadedFile, err := os.Stat(filepath.Join(imageFolder, dstFile)) + assert.Equal(t, nil, err, "read downloaded file error") - err = out.Close() - assert.Equal(t, nil, err, "close tmp file error") + assert.Equal(t, originalFile.Size(), downloadedFile.Size(), "different file sizes") +} - originalFile, err := os.Stat(filepath.Join(imageFolder, srcFile)) - assert.Equal(t, nil, err, "read original file error") +func TestGetGPTResponse(t *testing.T) { + t.Cleanup(func() { + tests.ClearDB() + if err := tests.ClearDataFolders(); err != nil { + t.Error(err) + } + }) - downloadedFile, err := os.Stat(filepath.Join(imageFolder, dstFile)) - assert.Equal(t, nil, err, "read downloaded file error") + addresses, privateKeys, err := tests.PrepareAccounts() + assert.Equal(t, nil, err, "prepare accounts error") - assert.Equal(t, originalFile.Size(), downloadedFile.Size(), "different file sizes") + _, task, err := tests.PrepareResultUploadedTask(models.TaskTypeLLM, addresses, config.GetDB()) + assert.Equal(t, nil, err, "prepare task error") - t.Cleanup(func() { - tests.ClearDB() - if err := tests.ClearDataFolders(); err != nil { - t.Error(err) - } - }) + getResultInput := &inference_tasks.GetGPTResultInput{ + TaskId: task.TaskId, } + + timestamp, signature, err := v1.SignData(getResultInput, privateKeys[0]) + assert.Equal(t, nil, err, "sign data error") + + r := callGetGPTResponseApi( + task.GetTaskIdAsString(), + timestamp, + signature) + + assert.Equal(t, 200, r.Code, "wrong http status code. message: "+r.Body.String()) + + res := inference_tasks.GPTTaskResponse{} + if err := json.Unmarshal(r.Body.Bytes(), &res); err != nil { + t.Error(err) + } + target := inference_tasks.GPTTaskResponse{} + if err := json.Unmarshal([]byte(tests.GPTResponseStr), &target); err != nil { + t.Error(err) + } + assert.Equal(t, target, res, "wrong returned gpt response") } func callGetImageApi( @@ -125,7 +182,18 @@ func callGetImageApi( timestamp int64, signature string) *httptest.ResponseRecorder { - endpoint := "/v1/inference_tasks/" + taskIdStr + "/results/" + imageNum + endpoint := "/v1/inference_tasks/stable_diffusion/" + taskIdStr + "/results/" + imageNum + query := "?timestamp=" + strconv.FormatInt(timestamp, 10) + "&signature=" + signature + + req, _ := http.NewRequest("GET", endpoint+query, nil) + w := httptest.NewRecorder() + tests.Application.ServeHTTP(w, req) + + return w +} + +func callGetGPTResponseApi(taskIdStr string, timestamp int64, signature string) *httptest.ResponseRecorder { + endpoint := "/v1/inference_tasks/gpt/" + taskIdStr + "/results" query := "?timestamp=" + strconv.FormatInt(timestamp, 10) + "&signature=" + signature req, _ := http.NewRequest("GET", endpoint+query, nil) diff --git a/api/v1/inference_tasks/gpt_response.go b/api/v1/inference_tasks/gpt_response.go new file mode 100644 index 0000000..c00aebe --- /dev/null +++ b/api/v1/inference_tasks/gpt_response.go @@ -0,0 +1,39 @@ +package inference_tasks + +type MessageRole string + +const ( + SystemRole MessageRole = "system" + UserRole MessageRole = "user" + AssistantRole MessageRole = "assistant" +) + +type Message struct { + Role MessageRole `json:"role"` + Content string `json:"content"` +} + +type Usage struct { + PromptTokens uint `json:"prompt_tokens" validate:"required"` + CompletionTokens uint `json:"completion_tokens" validate:"required"` + TotalTokens uint `json:"total_tokens" validate:"required"` +} + +type FinishReason string + +const ( + ReasonStop FinishReason = "stop" + ReasonLength FinishReason = "length" +) + +type ResponseChoice struct { + Index uint `json:"index" validate:"required"` + Message Message `json:"message" validate:"required"` + FinishReason FinishReason `json:"finish_reason" validate:"required"` +} + +type GPTTaskResponse struct { + Model string `json:"model" validate:"required"` + Choices []ResponseChoice `json:"choices" validate:"required"` + Usage Usage `json:"usage" validate:"required"` +} diff --git a/api/v1/inference_tasks/upload_result.go b/api/v1/inference_tasks/upload_result.go index 1e2582f..a550a2e 100644 --- a/api/v1/inference_tasks/upload_result.go +++ b/api/v1/inference_tasks/upload_result.go @@ -5,6 +5,7 @@ import ( "crynux_relay/blockchain" "crynux_relay/config" "crynux_relay/models" + "encoding/json" "errors" "os" "path/filepath" @@ -16,19 +17,19 @@ import ( "gorm.io/gorm" ) -type ResultInput struct { +type SDResultInput struct { TaskId uint64 `path:"task_id" json:"task_id" description:"Task id" validate:"required"` } -type ResultInputWithSignature struct { - ResultInput +type SDResultInputWithSignature struct { + SDResultInput Timestamp int64 `form:"timestamp" json:"timestamp" description:"Signature timestamp" validate:"required"` Signature string `form:"signature" json:"signature" description:"Signature" validate:"required"` } -func UploadResult(ctx *gin.Context, in *ResultInputWithSignature) (*response.Response, error) { +func UploadSDResult(ctx *gin.Context, in *SDResultInputWithSignature) (*response.Response, error) { - match, address, err := ValidateSignature(in.ResultInput, in.Timestamp, in.Signature) + match, address, err := ValidateSignature(in.SDResultInput, in.Timestamp, in.Signature) if err != nil { return nil, response.NewExceptionResponse(err) @@ -88,12 +89,7 @@ func UploadResult(ctx *gin.Context, in *ResultInputWithSignature) (*response.Res return nil, response.NewExceptionResponse(err) } - var hash []byte - if task.TaskType == models.TaskTypeSD { - hash, err = blockchain.GetPHashForImage(fileObj) - } else { - hash, err = blockchain.GetHashForGPTResponse(fileObj) - } + hash, err := blockchain.GetPHashForImage(fileObj) if err != nil { return nil, response.NewExceptionResponse(err) @@ -123,25 +119,117 @@ func UploadResult(ctx *gin.Context, in *ResultInputWithSignature) (*response.Res taskIdStr := task.GetTaskIdAsString() taskDir := filepath.Join(taskWorkspace, taskIdStr, "results") - if err = os.MkdirAll(taskDir, os.ModePerm); err != nil { + if err = os.MkdirAll(taskDir, 0700); err != nil { return nil, response.NewExceptionResponse(err) } - fileNum := 0 - var fileExt string - if task.TaskType == models.TaskTypeSD { - fileExt = ".png" - } else { - fileExt = ".json" + for i, file := range files { + filename := filepath.Join(taskDir, strconv.Itoa(i)+".png") + if err := ctx.SaveUploadedFile(file, filename); err != nil { + return nil, response.NewExceptionResponse(err) + } } - for _, file := range files { - filename := filepath.Join(taskDir, strconv.Itoa(fileNum)+fileExt) - if err := ctx.SaveUploadedFile(file, filename); err != nil { + // Update task status + task.Status = models.InferenceTaskResultsUploaded + + if err := config.GetDB().Save(&task).Error; err != nil { + return nil, response.NewExceptionResponse(err) + } + + return &response.Response{}, nil +} + +type GPTResultInput struct { + TaskId uint64 `path:"task_id" json:"task_id" description:"Task id" validate:"required"` + Result GPTTaskResponse `json:"result" description:"GPT task result" validate:"required"` +} + +type GPTResultInputWithSignature struct { + GPTResultInput + Timestamp int64 `form:"timestamp" json:"timestamp" description:"Signature timestamp" validate:"required"` + Signature string `form:"signature" json:"signature" description:"Signature" validate:"required"` +} + +func UploadGPTResult(ctx *gin.Context, in *GPTResultInputWithSignature) (*response.Response, error) { + match, address, err := ValidateSignature(in.GPTResultInput, in.Timestamp, in.Signature) + + if err != nil { + return nil, response.NewExceptionResponse(err) + } + + if !match { + validationErr := response.NewValidationErrorResponse("signature", "Invalid signature") + return nil, validationErr + } + + var task models.InferenceTask + + if result := config.GetDB().Where(&models.InferenceTask{TaskId: in.TaskId}).First(&task); result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + validationErr := response.NewValidationErrorResponse("task_id", "Task not found") + return nil, validationErr + } else { + return nil, response.NewExceptionResponse(result.Error) + } + } + + if task.Status != models.InferenceTaskPendingResults { + validationErr := response.NewValidationErrorResponse("task_id", "Task not success") + return nil, validationErr + } + + resultNode := &models.SelectedNode{ + InferenceTaskID: task.ID, + IsResultSelected: true, + } + + if err := config.GetDB().Where(resultNode).First(resultNode).Error; err != nil { + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, response.NewValidationErrorResponse("task_id", "Task not ready") + } else { return nil, response.NewExceptionResponse(err) } + } + + if resultNode.NodeAddress != address { + validationErr := response.NewValidationErrorResponse("signature", "Signer not allowed") + return nil, validationErr + } + + hash, err := blockchain.GetHashForGPTResponse(in.Result) + if err != nil { + return nil, response.NewExceptionResponse(err) + } + uploadedResult := hexutil.Encode(hash) + + log.Debugln("result hash from the blockchain: " + resultNode.Result) + log.Debugln("result hash from the uploaded result: " + uploadedResult) + + if resultNode.Result != uploadedResult { + validationErr := response.NewValidationErrorResponse("images", "Wrong images uploaded") + return nil, validationErr + } + + appConfig := config.GetConfig() + + taskWorkspace := appConfig.DataDir.InferenceTasks + taskIdStr := task.GetTaskIdAsString() - fileNum += 1 + taskDir := filepath.Join(taskWorkspace, taskIdStr, "results") + if err = os.MkdirAll(taskDir, 0700); err != nil { + return nil, response.NewExceptionResponse(err) + } + + resultBytes, err := json.Marshal(in.Result) + if err != nil { + return nil, response.NewExceptionResponse(err) + } + + filename := filepath.Join(taskDir, "0.json") + if err := os.WriteFile(filename, resultBytes, 0700); err != nil { + return nil, err } // Update task status @@ -152,4 +240,5 @@ func UploadResult(ctx *gin.Context, in *ResultInputWithSignature) (*response.Res } return &response.Response{}, nil + } diff --git a/api/v1/inference_tasks/upload_result_test.go b/api/v1/inference_tasks/upload_result_test.go index 93e5060..3bf9461 100644 --- a/api/v1/inference_tasks/upload_result_test.go +++ b/api/v1/inference_tasks/upload_result_test.go @@ -1,11 +1,13 @@ package inference_tasks_test import ( + "bytes" "crynux_relay/api/v1/inference_tasks" "crynux_relay/config" "crynux_relay/models" "crynux_relay/tests" v1 "crynux_relay/tests/api/v1" + "encoding/json" "image/png" "io" "mime/multipart" @@ -19,37 +21,66 @@ import ( "github.com/stretchr/testify/assert" ) -func TestWrongTaskId(t *testing.T) { - for _, taskType := range tests.TaskTypes { - addresses, privateKeys, err := tests.PrepareAccounts() - assert.Equal(t, nil, err, "prepare accounts error") +func TestWrongSDTaskId(t *testing.T) { + addresses, privateKeys, err := tests.PrepareAccounts() + assert.Equal(t, nil, err, "prepare accounts error") - _, _, err = tests.PreparePendingResultsTask(taskType, addresses, config.GetDB()) - assert.Equal(t, nil, err, "prepare task error") + _, _, err = tests.PreparePendingResultsTask(models.TaskTypeSD, addresses, config.GetDB()) + assert.Equal(t, nil, err, "prepare task error") - uploadResultInput := &inference_tasks.ResultInput{ - TaskId: 666, - } + uploadResultInput := &inference_tasks.SDResultInput{ + TaskId: 666, + } - pr, pw := io.Pipe() - writer := multipart.NewWriter(pw) + pr, pw := io.Pipe() + writer := multipart.NewWriter(pw) - timestamp, signature, err := v1.SignData(uploadResultInput, privateKeys[1]) - assert.Equal(t, nil, err, "sign data error") + timestamp, signature, err := v1.SignData(uploadResultInput, privateKeys[1]) + assert.Equal(t, nil, err, "sign data error") - prepareFileForm(t, writer, taskType, timestamp, signature) + prepareFileForm(t, writer, models.TaskTypeSD, timestamp, signature) - r := callUploadResultApi(666, writer, pr) + r := callUploadSDResultApi(666, writer, pr) - v1.AssertValidationErrorResponse(t, r, "task_id", "Task not found") + v1.AssertValidationErrorResponse(t, r, "task_id", "Task not found") - t.Cleanup(func() { - tests.ClearDB() - if err := tests.ClearDataFolders(); err != nil { - t.Error(err) - } - }) + t.Cleanup(func() { + tests.ClearDB() + if err := tests.ClearDataFolders(); err != nil { + t.Error(err) + } + }) +} + +func TestWrongGPTTaskId(t *testing.T) { + addresses, privateKeys, err := tests.PrepareAccounts() + assert.Equal(t, nil, err, "prepare accounts error") + + _, _, err = tests.PreparePendingResultsTask(models.TaskTypeLLM, addresses, config.GetDB()) + assert.Equal(t, nil, err, "prepare task error") + + response := inference_tasks.GPTTaskResponse{} + err = json.Unmarshal([]byte(tests.GPTResponseStr), &response) + assert.Equal(t, nil, err, "json unmarshal error") + + uploadResultInput := &inference_tasks.GPTResultInput{ + TaskId: 666, + Result: response, } + + timestamp, signature, err := v1.SignData(uploadResultInput, privateKeys[1]) + assert.Equal(t, nil, err, "sign data error") + + r := callUploadGPTResultApi(666, uploadResultInput, timestamp, signature) + + v1.AssertValidationErrorResponse(t, r, "task_id", "Task not found") + + t.Cleanup(func() { + tests.ClearDB() + if err := tests.ClearDataFolders(); err != nil { + t.Error(err) + } + }) } func TestCreatorUpload(t *testing.T) { @@ -121,21 +152,35 @@ func testUsingAddressNum( assert.Equal(t, models.InferenceTaskPendingResults, task.Status, "wrong task status") - uploadResultInput := &inference_tasks.ResultInput{ - TaskId: task.TaskId, - } + if taskType == models.TaskTypeSD { + uploadResultInput := &inference_tasks.SDResultInput{ + TaskId: task.TaskId, + } - pr, pw := io.Pipe() - writer := multipart.NewWriter(pw) + pr, pw := io.Pipe() + writer := multipart.NewWriter(pw) - timestamp, signature, err := v1.SignData(uploadResultInput, privateKeys[num]) - assert.Equal(t, nil, err, "sign data error") + timestamp, signature, err := v1.SignData(uploadResultInput, privateKeys[num]) + assert.Equal(t, nil, err, "sign data error") - prepareFileForm(t, writer, taskType, timestamp, signature) + prepareFileForm(t, writer, taskType, timestamp, signature) + r := callUploadSDResultApi(task.TaskId, writer, pr) + assertFunc(t, r, task, addresses) + } else { + response := inference_tasks.GPTTaskResponse{} + err = json.Unmarshal([]byte(tests.GPTResponseStr), &response) + assert.Equal(t, nil, err, "json unmarshal error") - r := callUploadResultApi(task.TaskId, writer, pr) + uploadResultInput := &inference_tasks.GPTResultInput{ + TaskId: task.TaskId, + Result: response, + } - assertFunc(t, r, task, addresses) + timestamp, signature, err := v1.SignData(uploadResultInput, privateKeys[num]) + assert.Equal(t, nil, err, "sign data error") + r := callUploadGPTResultApi(task.TaskId, uploadResultInput, timestamp, signature) + assertFunc(t, r, task, addresses) + } t.Cleanup(func() { tests.ClearDB() @@ -214,10 +259,10 @@ func assertFileExists(t *testing.T, taskId uint64, taskType models.ChainTaskType assert.Equal(t, nil, err, "image not exist") } -func callUploadResultApi(taskId uint64, writer *multipart.Writer, pr *io.PipeReader) *httptest.ResponseRecorder { +func callUploadSDResultApi(taskId uint64, writer *multipart.Writer, pr *io.PipeReader) *httptest.ResponseRecorder { taskIdStr := strconv.FormatUint(taskId, 10) - req, _ := http.NewRequest("POST", "/v1/inference_tasks/"+taskIdStr+"/results", pr) + req, _ := http.NewRequest("POST", "/v1/inference_tasks/stable_diffusion/"+taskIdStr+"/results", pr) req.Header.Add("Content-Type", writer.FormDataContentType()) w := httptest.NewRecorder() @@ -225,3 +270,22 @@ func callUploadResultApi(taskId uint64, writer *multipart.Writer, pr *io.PipeRea return w } + +func callUploadGPTResultApi(taskId uint64, input *inference_tasks.GPTResultInput, timestamp int64, signature string) *httptest.ResponseRecorder { + taskIdStr := strconv.FormatUint(taskId, 10) + + inputWithSignature := inference_tasks.GPTResultInputWithSignature{ + GPTResultInput: *input, + Timestamp: timestamp, + Signature: signature, + } + inputBytes, _ := json.Marshal(inputWithSignature) + + req, _ := http.NewRequest("POST", "/v1/inference_tasks/gpt/"+taskIdStr+"/results", bytes.NewReader(inputBytes)) + req.Header.Add("Content-Type", "application/json") + + w := httptest.NewRecorder() + tests.Application.ServeHTTP(w, req) + + return w +} diff --git a/api/v1/routes.go b/api/v1/routes.go index 987bed5..d50d343 100644 --- a/api/v1/routes.go +++ b/api/v1/routes.go @@ -26,16 +26,27 @@ func InitRoutes(r *fizz.Fizz) { fizz.Response("400", "validation errors", response.ValidationErrorResponse{}, nil, nil), }, tonic.Handler(inference_tasks.GetTaskById, 200)) - tasksGroup.POST("/:task_id/results", []fizz.OperationOption{ - fizz.Summary("Upload inference task result"), + tasksGroup.POST("/stable_diffusion/:task_id/results", []fizz.OperationOption{ + fizz.Summary("Upload stable diffusion task result"), fizz.Response("400", "validation errors", response.ValidationErrorResponse{}, nil, nil), fizz.Response("500", "exception", response.ExceptionResponse{}, nil, nil), - }, tonic.Handler(inference_tasks.UploadResult, 200)) + }, tonic.Handler(inference_tasks.UploadSDResult, 200)) - tasksGroup.GET("/:task_id/results/:image_num", []fizz.OperationOption{ - fizz.Summary("Get the result of the inference task by node address"), + tasksGroup.POST("/gpt/:task_id/results", []fizz.OperationOption{ + fizz.Summary("Upload gpt task result"), fizz.Response("400", "validation errors", response.ValidationErrorResponse{}, nil, nil), - }, tonic.Handler(inference_tasks.GetResult, 200)) + fizz.Response("500", "exception", response.ExceptionResponse{}, nil, nil), + }, tonic.Handler(inference_tasks.UploadGPTResult, 200)) + + tasksGroup.GET("/stable_diffusion/:task_id/results/:image_num", []fizz.OperationOption{ + fizz.Summary("Get the result of the stable diffusion task by node address"), + fizz.Response("400", "validation errors", response.ValidationErrorResponse{}, nil, nil), + }, tonic.Handler(inference_tasks.GetSDResult, 200)) + + tasksGroup.GET("/gpt/:task_id/results", []fizz.OperationOption{ + fizz.Summary("Get the result of the gpt task by node address"), + fizz.Response("400", "validation errors", response.ValidationErrorResponse{}, nil, nil), + }, tonic.Handler(inference_tasks.GetGPTResult, 200)) networkGroup := v1g.Group("network", "network", "Network stats related APIs") diff --git a/blockchain/task.go b/blockchain/task.go index 54b531a..bb9577d 100644 --- a/blockchain/task.go +++ b/blockchain/task.go @@ -7,6 +7,7 @@ import ( "crynux_relay/models" "crypto/sha256" "encoding/binary" + "encoding/json" "errors" "image/png" "io" @@ -172,8 +173,8 @@ func GetPHashForImage(reader io.Reader) ([]byte, error) { return bs, nil } -func GetHashForGPTResponse(reader io.Reader) ([]byte, error) { - content, err := io.ReadAll(reader) +func GetHashForGPTResponse(response interface{}) ([]byte, error) { + content, err := json.Marshal(response) if err != nil { return nil, err } diff --git a/tests/gpt_resp.go b/tests/gpt_resp.go index 7a42746..9c5fd95 100644 --- a/tests/gpt_resp.go +++ b/tests/gpt_resp.go @@ -1,9 +1,11 @@ package tests import ( + "crynux_relay/api/v1/inference_tasks" "crynux_relay/blockchain" "crynux_relay/config" "crynux_relay/models" + "encoding/json" "os" "path/filepath" @@ -11,19 +13,22 @@ import ( ) const GPTResponseStr = `{ - "model": "gpt2", - "choices": [ - { - "finish_reason": "length", - "message": { - "role": "assistant", - "content": '\n\nI have a chat bot, called "Eleanor" which was developed by my team on Skype. ' - "The only thing I will say is this", - }, - "index": 0, - } - ], - "usage": {"prompt_tokens": 11, "completion_tokens": 30, "total_tokens": 41}, + "model": "gpt2", + "choices": [ + { + "finish_reason": "length", + "message": { + "role": "assistant", + "content": "\n\nI have a chat bot, called \"Eleanor\" which was developed by my team on Skype. The only thing I will say is this" + }, + "index": 0 + } + ], + "usage": { + "prompt_tokens": 11, + "completion_tokens": 30, + "total_tokens": 41 + } }` func prepareGPTResponseForTask(task *models.InferenceTask) (string, error) { @@ -44,11 +49,11 @@ func prepareGPTResponseForTask(task *models.InferenceTask) (string, error) { return "", nil } - resultFile, err := os.Open(filename) - if err != nil { + resp := inference_tasks.GPTTaskResponse{} + if err := json.Unmarshal([]byte(GPTResponseStr), &resp); err != nil { return "", nil } - h, err := blockchain.GetHashForGPTResponse(resultFile) + h, err := blockchain.GetHashForGPTResponse(resp) if err != nil { return "", nil } diff --git a/tests/init.go b/tests/init.go index ae1d9d2..9d5e5e3 100644 --- a/tests/init.go +++ b/tests/init.go @@ -17,7 +17,7 @@ var Application *gin.Engine = nil func init() { wd, _ := os.Getwd() - wd = strings.SplitAfter(wd, "h-relay")[0] + wd = strings.SplitAfter(wd, "crynux-relay")[0] if err := os.Chdir(wd); err != nil { print(err.Error()) os.Exit(1) From d8e1ea7f6529aa5fda81be785376530e934eb3d6 Mon Sep 17 00:00:00 2001 From: iwehf Date: Sun, 4 Feb 2024 11:49:26 +0800 Subject: [PATCH 2/5] add utils: json marshal with sorted keys --- utils/json.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 utils/json.go diff --git a/utils/json.go b/utils/json.go new file mode 100644 index 0000000..7010b87 --- /dev/null +++ b/utils/json.go @@ -0,0 +1,27 @@ +package utils + +import "encoding/json" + +func jsonRemarshal(bytes []byte) ([]byte, error) { + var v interface{} + if err := json.Unmarshal(bytes, &v); err != nil { + return nil, err + } + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + return data, nil +} + +func JSONMarshalWithSortedKeys(v interface{}) ([]byte, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + data, err = jsonRemarshal(data) + if err != nil { + return nil, err + } + return data, nil +} From 594630def72847b73e46e9df255cd3188a46815f Mon Sep 17 00:00:00 2001 From: iwehf Date: Sun, 4 Feb 2024 11:50:46 +0800 Subject: [PATCH 3/5] use JSONMarshalWithSortedKeys in api signature and gpt response hash --- api/v1/inference_tasks/validate_signature.go | 9 ++++--- blockchain/task.go | 4 +-- blockchain/task_test.go | 26 ++++++++++++++++++++ tests/api/v1/sign.go | 9 ++++--- 4 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 blockchain/task_test.go diff --git a/api/v1/inference_tasks/validate_signature.go b/api/v1/inference_tasks/validate_signature.go index 669d3e2..e7fba7b 100644 --- a/api/v1/inference_tasks/validate_signature.go +++ b/api/v1/inference_tasks/validate_signature.go @@ -1,18 +1,19 @@ package inference_tasks import ( + "crynux_relay/utils" "encoding/hex" - "encoding/json" - "github.com/ethereum/go-ethereum/crypto" - log "github.com/sirupsen/logrus" "math" "strconv" "time" + + "github.com/ethereum/go-ethereum/crypto" + log "github.com/sirupsen/logrus" ) func ValidateSignature(data interface{}, timestamp int64, signature string) (bool, string, error) { - dataBytes, err := json.Marshal(data) + dataBytes, err := utils.JSONMarshalWithSortedKeys(data) if err != nil { return false, "", err } diff --git a/blockchain/task.go b/blockchain/task.go index bb9577d..52a1b34 100644 --- a/blockchain/task.go +++ b/blockchain/task.go @@ -5,9 +5,9 @@ import ( "crynux_relay/blockchain/bindings" "crynux_relay/config" "crynux_relay/models" + "crynux_relay/utils" "crypto/sha256" "encoding/binary" - "encoding/json" "errors" "image/png" "io" @@ -174,7 +174,7 @@ func GetPHashForImage(reader io.Reader) ([]byte, error) { } func GetHashForGPTResponse(response interface{}) ([]byte, error) { - content, err := json.Marshal(response) + content, err := utils.JSONMarshalWithSortedKeys(response) if err != nil { return nil, err } diff --git a/blockchain/task_test.go b/blockchain/task_test.go new file mode 100644 index 0000000..d73a253 --- /dev/null +++ b/blockchain/task_test.go @@ -0,0 +1,26 @@ +package blockchain_test + +import ( + "crynux_relay/api/v1/inference_tasks" + "crynux_relay/blockchain" + "crynux_relay/tests" + "encoding/json" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" +) + +func TestGPTRespHash(t *testing.T) { + resp := inference_tasks.GPTTaskResponse{} + if err := json.Unmarshal([]byte(tests.GPTResponseStr), &resp); err != nil { + t.Error(err) + } + + hashBytes, err := blockchain.GetHashForGPTResponse(resp) + if err != nil { + t.Error(err) + } + hash := hexutil.Encode(hashBytes) + assert.Equal(t, hash, "0x7aa4c9036633f745aa73f03331abb7fa5beb1bc6b7f3688322432a3da61f49c2", "Wrong gpt resh hash") +} diff --git a/tests/api/v1/sign.go b/tests/api/v1/sign.go index 37a8e6f..146b336 100644 --- a/tests/api/v1/sign.go +++ b/tests/api/v1/sign.go @@ -1,19 +1,20 @@ package v1 import ( + "crynux_relay/utils" "crypto/ecdsa" - "encoding/json" "errors" + "strconv" + "time" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" log "github.com/sirupsen/logrus" - "strconv" - "time" ) func SignData(data interface{}, privateKeyStr string) (timestamp int64, signature string, err error) { - dataBytes, err := json.Marshal(data) + dataBytes, err := utils.JSONMarshalWithSortedKeys(data) if err != nil { return 0, "", err } From 2a30a1b781154dcbd4ab106e8a7bc908163dece9 Mon Sep 17 00:00:00 2001 From: iwehf Date: Sun, 4 Feb 2024 17:47:39 +0800 Subject: [PATCH 4/5] move GPTTaskResponse to package models; change get result response --- api/v1/inference_tasks/get_result.go | 8 ++-- api/v1/inference_tasks/get_result_test.go | 6 +-- api/v1/inference_tasks/gpt_response.go | 42 ++++---------------- api/v1/inference_tasks/upload_result.go | 2 +- api/v1/inference_tasks/upload_result_test.go | 4 +- blockchain/task_test.go | 4 +- models/gpt_resp.go | 39 ++++++++++++++++++ tests/gpt_resp.go | 3 +- 8 files changed, 59 insertions(+), 49 deletions(-) create mode 100644 models/gpt_resp.go diff --git a/api/v1/inference_tasks/get_result.go b/api/v1/inference_tasks/get_result.go index c30ede0..164ba11 100644 --- a/api/v1/inference_tasks/get_result.go +++ b/api/v1/inference_tasks/get_result.go @@ -88,7 +88,7 @@ type GetGPTResultInputWithSignature struct { Signature string `query:"signature" description:"Signature" validate:"required"` } -func GetGPTResult(ctx *gin.Context, in *GetGPTResultInputWithSignature) (*GPTTaskResponse, error) { +func GetGPTResult(ctx *gin.Context, in *GetGPTResultInputWithSignature) (*GPTResultResponse, error) { match, address, err := ValidateSignature(in.GetGPTResultInput, in.Timestamp, in.Signature) if err != nil || !match { @@ -136,10 +136,10 @@ func GetGPTResult(ctx *gin.Context, in *GetGPTResultInputWithSignature) (*GPTTas return nil, response.NewExceptionResponse(err) } - result := &GPTTaskResponse{} - if err := json.Unmarshal(resultContent, result); err != nil { + data := &models.GPTTaskResponse{} + if err := json.Unmarshal(resultContent, data); err != nil { return nil, response.NewExceptionResponse(err) } - return result, nil + return &GPTResultResponse{Data: *data}, nil } diff --git a/api/v1/inference_tasks/get_result_test.go b/api/v1/inference_tasks/get_result_test.go index be7a049..e49a168 100644 --- a/api/v1/inference_tasks/get_result_test.go +++ b/api/v1/inference_tasks/get_result_test.go @@ -165,15 +165,15 @@ func TestGetGPTResponse(t *testing.T) { assert.Equal(t, 200, r.Code, "wrong http status code. message: "+r.Body.String()) - res := inference_tasks.GPTTaskResponse{} + res := inference_tasks.GPTResultResponse{} if err := json.Unmarshal(r.Body.Bytes(), &res); err != nil { t.Error(err) } - target := inference_tasks.GPTTaskResponse{} + target := models.GPTTaskResponse{} if err := json.Unmarshal([]byte(tests.GPTResponseStr), &target); err != nil { t.Error(err) } - assert.Equal(t, target, res, "wrong returned gpt response") + assert.Equal(t, target, res.Data, "wrong returned gpt response") } func callGetImageApi( diff --git a/api/v1/inference_tasks/gpt_response.go b/api/v1/inference_tasks/gpt_response.go index c00aebe..9a704dd 100644 --- a/api/v1/inference_tasks/gpt_response.go +++ b/api/v1/inference_tasks/gpt_response.go @@ -1,39 +1,11 @@ package inference_tasks -type MessageRole string - -const ( - SystemRole MessageRole = "system" - UserRole MessageRole = "user" - AssistantRole MessageRole = "assistant" +import ( + "crynux_relay/api/v1/response" + "crynux_relay/models" ) -type Message struct { - Role MessageRole `json:"role"` - Content string `json:"content"` -} - -type Usage struct { - PromptTokens uint `json:"prompt_tokens" validate:"required"` - CompletionTokens uint `json:"completion_tokens" validate:"required"` - TotalTokens uint `json:"total_tokens" validate:"required"` -} - -type FinishReason string - -const ( - ReasonStop FinishReason = "stop" - ReasonLength FinishReason = "length" -) - -type ResponseChoice struct { - Index uint `json:"index" validate:"required"` - Message Message `json:"message" validate:"required"` - FinishReason FinishReason `json:"finish_reason" validate:"required"` -} - -type GPTTaskResponse struct { - Model string `json:"model" validate:"required"` - Choices []ResponseChoice `json:"choices" validate:"required"` - Usage Usage `json:"usage" validate:"required"` -} +type GPTResultResponse struct{ + response.Response + Data models.GPTTaskResponse `json:"data"` +} \ No newline at end of file diff --git a/api/v1/inference_tasks/upload_result.go b/api/v1/inference_tasks/upload_result.go index a550a2e..9184f61 100644 --- a/api/v1/inference_tasks/upload_result.go +++ b/api/v1/inference_tasks/upload_result.go @@ -142,7 +142,7 @@ func UploadSDResult(ctx *gin.Context, in *SDResultInputWithSignature) (*response type GPTResultInput struct { TaskId uint64 `path:"task_id" json:"task_id" description:"Task id" validate:"required"` - Result GPTTaskResponse `json:"result" description:"GPT task result" validate:"required"` + Result models.GPTTaskResponse `json:"result" description:"GPT task result" validate:"required"` } type GPTResultInputWithSignature struct { diff --git a/api/v1/inference_tasks/upload_result_test.go b/api/v1/inference_tasks/upload_result_test.go index 3bf9461..7aecdf5 100644 --- a/api/v1/inference_tasks/upload_result_test.go +++ b/api/v1/inference_tasks/upload_result_test.go @@ -59,7 +59,7 @@ func TestWrongGPTTaskId(t *testing.T) { _, _, err = tests.PreparePendingResultsTask(models.TaskTypeLLM, addresses, config.GetDB()) assert.Equal(t, nil, err, "prepare task error") - response := inference_tasks.GPTTaskResponse{} + response := models.GPTTaskResponse{} err = json.Unmarshal([]byte(tests.GPTResponseStr), &response) assert.Equal(t, nil, err, "json unmarshal error") @@ -167,7 +167,7 @@ func testUsingAddressNum( r := callUploadSDResultApi(task.TaskId, writer, pr) assertFunc(t, r, task, addresses) } else { - response := inference_tasks.GPTTaskResponse{} + response := models.GPTTaskResponse{} err = json.Unmarshal([]byte(tests.GPTResponseStr), &response) assert.Equal(t, nil, err, "json unmarshal error") diff --git a/blockchain/task_test.go b/blockchain/task_test.go index d73a253..ef099d3 100644 --- a/blockchain/task_test.go +++ b/blockchain/task_test.go @@ -1,8 +1,8 @@ package blockchain_test import ( - "crynux_relay/api/v1/inference_tasks" "crynux_relay/blockchain" + "crynux_relay/models" "crynux_relay/tests" "encoding/json" "testing" @@ -12,7 +12,7 @@ import ( ) func TestGPTRespHash(t *testing.T) { - resp := inference_tasks.GPTTaskResponse{} + resp := models.GPTTaskResponse{} if err := json.Unmarshal([]byte(tests.GPTResponseStr), &resp); err != nil { t.Error(err) } diff --git a/models/gpt_resp.go b/models/gpt_resp.go new file mode 100644 index 0000000..7424502 --- /dev/null +++ b/models/gpt_resp.go @@ -0,0 +1,39 @@ +package models + +type MessageRole string + +const ( + SystemRole MessageRole = "system" + UserRole MessageRole = "user" + AssistantRole MessageRole = "assistant" +) + +type Message struct { + Role MessageRole `json:"role"` + Content string `json:"content"` +} + +type Usage struct { + PromptTokens uint `json:"prompt_tokens" validate:"required"` + CompletionTokens uint `json:"completion_tokens" validate:"required"` + TotalTokens uint `json:"total_tokens" validate:"required"` +} + +type FinishReason string + +const ( + ReasonStop FinishReason = "stop" + ReasonLength FinishReason = "length" +) + +type ResponseChoice struct { + Index uint `json:"index" validate:"required"` + Message Message `json:"message" validate:"required"` + FinishReason FinishReason `json:"finish_reason" validate:"required"` +} + +type GPTTaskResponse struct { + Model string `json:"model" validate:"required"` + Choices []ResponseChoice `json:"choices" validate:"required"` + Usage Usage `json:"usage" validate:"required"` +} diff --git a/tests/gpt_resp.go b/tests/gpt_resp.go index 9c5fd95..540f798 100644 --- a/tests/gpt_resp.go +++ b/tests/gpt_resp.go @@ -1,7 +1,6 @@ package tests import ( - "crynux_relay/api/v1/inference_tasks" "crynux_relay/blockchain" "crynux_relay/config" "crynux_relay/models" @@ -49,7 +48,7 @@ func prepareGPTResponseForTask(task *models.InferenceTask) (string, error) { return "", nil } - resp := inference_tasks.GPTTaskResponse{} + resp := models.GPTTaskResponse{} if err := json.Unmarshal([]byte(GPTResponseStr), &resp); err != nil { return "", nil } From 8b40122a5617387a8b181fa6a8f10219ee0792d7 Mon Sep 17 00:00:00 2001 From: iwehf Date: Tue, 6 Feb 2024 19:37:39 +0800 Subject: [PATCH 5/5] change error response message in uploadGPTResult --- api/v1/inference_tasks/upload_result.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/inference_tasks/upload_result.go b/api/v1/inference_tasks/upload_result.go index 9184f61..f439f6d 100644 --- a/api/v1/inference_tasks/upload_result.go +++ b/api/v1/inference_tasks/upload_result.go @@ -208,7 +208,7 @@ func UploadGPTResult(ctx *gin.Context, in *GPTResultInputWithSignature) (*respon log.Debugln("result hash from the uploaded result: " + uploadedResult) if resultNode.Result != uploadedResult { - validationErr := response.NewValidationErrorResponse("images", "Wrong images uploaded") + validationErr := response.NewValidationErrorResponse("images", "Wrong result uploaded") return nil, validationErr }