diff --git a/core/http/endpoints/openai/audio_upload_test.go b/core/http/endpoints/openai/audio_upload_test.go index 2df8e5f02..ee788a4fd 100644 --- a/core/http/endpoints/openai/audio_upload_test.go +++ b/core/http/endpoints/openai/audio_upload_test.go @@ -50,6 +50,22 @@ var _ = Describe("audio upload endpoints reject bad uploads as client errors", f Expect(he.Code).To(Equal(http.StatusBadRequest)) } + // An unknown response_format is the caller's fault too, and must be + // rejected before the backend runs: a failover chain would otherwise + // transcribe on every target and count the error against each of them. + for name, ec := range map[string]endpointCase{ + "transcription": cases["transcription"], + "diarization": cases["diarization"], + } { + It(name+": unknown response_format", func() { + body := "--xyz\r\nContent-Disposition: form-data; name=\"response_format\"\r\n\r\nbogus\r\n" + + "--xyz\r\nContent-Disposition: form-data; name=\"file\"; filename=\"a.wav\"\r\n\r\nRIFF\r\n--xyz--\r\n" + var err error + Expect(func() { err = run(ec, "multipart/form-data; boundary=xyz", body) }).NotTo(Panic(), "the backend must not be reached") + expectBadRequest(err) + }) + } + for name, ec := range cases { It(name+": multipart content type without a boundary", func() { expectBadRequest(run(ec, "multipart/form-data", "")) diff --git a/core/http/endpoints/openai/diarization.go b/core/http/endpoints/openai/diarization.go index 59cc73599..b39935e8b 100644 --- a/core/http/endpoints/openai/diarization.go +++ b/core/http/endpoints/openai/diarization.go @@ -1,7 +1,6 @@ package openai import ( - "errors" "fmt" "io" "net/http" @@ -75,6 +74,13 @@ func DiarizationEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, ap if responseFormat == "" { responseFormat = schema.DiarizationResponseFormatJson } + switch responseFormat { + case schema.DiarizationResponseFormatJson, schema.DiarizationResponseFormatJsonVerbose, schema.DiarizationResponseFormatRTTM: + default: + // Checked before the backend runs, for the same reason as in + // TranscriptEndpoint. + return echo.NewHTTPError(http.StatusBadRequest, "invalid response_format (expected: json, verbose_json, rttm)") + } file, err := uploadedFile(c, "file") if err != nil { @@ -126,7 +132,7 @@ func DiarizationEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, ap case schema.DiarizationResponseFormatJsonVerbose: return c.JSON(http.StatusOK, result) default: - return errors.New("invalid response_format (expected: json, verbose_json, rttm)") + return echo.NewHTTPError(http.StatusBadRequest, "invalid response_format (expected: json, verbose_json, rttm)") } } } diff --git a/core/http/endpoints/openai/transcription.go b/core/http/endpoints/openai/transcription.go index 77b9237c1..6c99d7afc 100644 --- a/core/http/endpoints/openai/transcription.go +++ b/core/http/endpoints/openai/transcription.go @@ -2,7 +2,6 @@ package openai import ( "encoding/json" - "errors" "fmt" "io" "mime/multipart" @@ -117,6 +116,13 @@ func TranscriptEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, app } } + // Reject an unknown format before the backend runs: the backend work + // would be wasted, and a failover chain would count the error + // against every target. + if !stream && !validTranscriptionResponseFormat(responseFormat) { + return echo.NewHTTPError(http.StatusBadRequest, "invalid response_format") + } + // retrieve the file data from the request file, err := uploadedFile(c, "file") if err != nil { @@ -230,11 +236,21 @@ func TranscriptEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, app } return c.JSON(http.StatusOK, trs) default: - return errors.New("invalid response_format") + return echo.NewHTTPError(http.StatusBadRequest, "invalid response_format") } } } +func validTranscriptionResponseFormat(f schema.TranscriptionResponseFormatType) bool { + switch f { + case "", schema.TranscriptionResponseFormatLrc, schema.TranscriptionResponseFormatText, + schema.TranscriptionResponseFormatSrt, schema.TranscriptionResponseFormatVtt, + schema.TranscriptionResponseFormatJson, schema.TranscriptionResponseFormatJsonVerbose: + return true + } + return false +} + // streamTranscription emits OpenAI-format SSE events for a transcription // request: one `transcript.text.delta` per backend chunk, a final // `transcript.text.done` with the assembled text, and `[DONE]`. Backends that