fix(openai): reject an unknown audio response_format as a 400 before the backend runs

Transcription and diarization checked response_format only after the
backend had run, and returned a plain error for an unknown value.
Failover counts a plain error as a target failure, so one request with
a bad response_format ran the backend on every target of a chain and
tripped all of them.

Check the format before the backend runs and answer 400.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
This commit is contained in:
Ettore Di Giacinto committed 2026-09-27 20:17:06 +00:00
1 parent 10f7b1ca60
commit f20cc16033
3 files changed
+42 -4

No files matched your search

@@ -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", ""))
+8 -2
View File
@@ -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)")
}
}
}
+18 -2
View File
@@ -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