fix: scope video dispatch to /v1/videos/sync + capture mask
Review findings: the async POST /v1/videos leg is deliberately NOT dispatched — its poll/download companions carry no model field to route by, so registering creation alone would start unretrievable upstream jobs (a stray OpenAI-videos client now gets a clean 404). And /v1/videos/sync joins captureFieldsByPath (headers only, both directions) so enabling captures doesn't buffer conditioning frames or whole clips through cbor+zstd. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXQxVhXBw8PwFAtsVrXSmj
This commit is contained in:
@@ -37,7 +37,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// captureFieldsByPath overrides the default capture mask for routes carrying
|
// captureFieldsByPath overrides the default capture mask for routes carrying
|
||||||
// large binary payloads (audio/image) where storing the full body is wasteful.
|
// large binary payloads (audio/image/video) where storing the full body is
|
||||||
|
// wasteful.
|
||||||
var captureFieldsByPath = map[string]captureFields{
|
var captureFieldsByPath = map[string]captureFields{
|
||||||
"/v1/audio/speech": captureReqAll | captureRespHeaders,
|
"/v1/audio/speech": captureReqAll | captureRespHeaders,
|
||||||
"/v1/audio/voices": captureReqHeaders | captureRespAll,
|
"/v1/audio/voices": captureReqHeaders | captureRespAll,
|
||||||
@@ -46,6 +47,9 @@ var captureFieldsByPath = map[string]captureFields{
|
|||||||
"/v1/images/edits": captureReqHeaders | captureRespHeaders,
|
"/v1/images/edits": captureReqHeaders | captureRespHeaders,
|
||||||
"/sdapi/v1/txt2img": captureReqAll | captureRespHeaders,
|
"/sdapi/v1/txt2img": captureReqAll | captureRespHeaders,
|
||||||
"/sdapi/v1/img2img": captureReqHeaders | captureRespHeaders,
|
"/sdapi/v1/img2img": captureReqHeaders | captureRespHeaders,
|
||||||
|
// video: the request multipart can carry a conditioning frame and the
|
||||||
|
// response body IS the clip — headers only, both directions.
|
||||||
|
"/v1/videos/sync": captureReqHeaders | captureRespHeaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
// captureFieldsFor returns the capture mask for a request path. Unlisted routes
|
// captureFieldsFor returns the capture mask for a request path. Unlisted routes
|
||||||
|
|||||||
@@ -81,9 +81,13 @@ var modelPostFormRoutes = []string{
|
|||||||
"/v1/audio/transcriptions",
|
"/v1/audio/transcriptions",
|
||||||
"/v1/images/edits",
|
"/v1/images/edits",
|
||||||
|
|
||||||
// video generation (vLLM-Omni / OpenAI videos shape); extraction is
|
// video generation: ONLY the blocking sync shape (vLLM-Omni
|
||||||
// content-type driven, so JSON bodies dispatch on these paths too
|
// /v1/videos/sync — the response body is the clip). The async
|
||||||
"/v1/videos",
|
// OpenAI-style POST /v1/videos is deliberately NOT dispatched: its
|
||||||
|
// poll/download companions (GET /v1/videos/{id}[/content]) carry no
|
||||||
|
// model field to route by, so registering creation alone would start
|
||||||
|
// unretrievable upstream jobs. Extraction is content-type driven, so
|
||||||
|
// JSON bodies dispatch here too.
|
||||||
"/v1/videos/sync",
|
"/v1/videos/sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -348,37 +348,49 @@ func TestServer_VideoRoutesDispatch(t *testing.T) {
|
|||||||
newStubRouter(nil, ""),
|
newStubRouter(nil, ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSON body on /v1/videos.
|
|
||||||
body := strings.NewReader(`{"model":"videogen-model","prompt":"a cat"}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/v1/videos", body)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
s.ServeHTTP(w, req)
|
|
||||||
if w.Code != http.StatusOK || w.Body.String() != "video response" {
|
|
||||||
t.Fatalf("/v1/videos: status=%d body=%q", w.Code, w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multipart form on /v1/videos/sync.
|
// Multipart form on /v1/videos/sync.
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
mw := multipart.NewWriter(&buf)
|
mw := multipart.NewWriter(&buf)
|
||||||
mw.WriteField("model", "videogen-model")
|
mw.WriteField("model", "videogen-model")
|
||||||
mw.WriteField("prompt", "a cat")
|
mw.WriteField("prompt", "a cat")
|
||||||
mw.Close()
|
mw.Close()
|
||||||
req = httptest.NewRequest(http.MethodPost, "/v1/videos/sync", &buf)
|
req := httptest.NewRequest(http.MethodPost, "/v1/videos/sync", &buf)
|
||||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||||
w = httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.ServeHTTP(w, req)
|
s.ServeHTTP(w, req)
|
||||||
if w.Code != http.StatusOK || w.Body.String() != "video response" {
|
if w.Code != http.StatusOK || w.Body.String() != "video response" {
|
||||||
t.Fatalf("/v1/videos/sync: status=%d body=%q", w.Code, w.Body.String())
|
t.Fatalf("/v1/videos/sync: status=%d body=%q", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown model on a video route still 404s.
|
// JSON bodies dispatch too (extraction is content-type driven).
|
||||||
body = strings.NewReader(`{"model":"nope","prompt":"a cat"}`)
|
jsonReq := httptest.NewRequest(http.MethodPost, "/v1/videos/sync",
|
||||||
req = httptest.NewRequest(http.MethodPost, "/v1/videos", body)
|
strings.NewReader(`{"model":"videogen-model","prompt":"a cat"}`))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
jsonReq.Header.Set("Content-Type", "application/json")
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
s.ServeHTTP(w, req)
|
s.ServeHTTP(w, jsonReq)
|
||||||
|
if w.Code != http.StatusOK || w.Body.String() != "video response" {
|
||||||
|
t.Fatalf("/v1/videos/sync json: status=%d body=%q", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown model on the video route still 404s.
|
||||||
|
nopeReq := httptest.NewRequest(http.MethodPost, "/v1/videos/sync",
|
||||||
|
strings.NewReader(`{"model":"nope","prompt":"a cat"}`))
|
||||||
|
nopeReq.Header.Set("Content-Type", "application/json")
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(w, nopeReq)
|
||||||
if w.Code != http.StatusNotFound {
|
if w.Code != http.StatusNotFound {
|
||||||
t.Fatalf("/v1/videos unknown model: status=%d want 404", w.Code)
|
t.Fatalf("/v1/videos/sync unknown model: status=%d want 404", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The async job-creation route is NOT dispatched (no poll routes to
|
||||||
|
// complete the flow) — a stray OpenAI-videos client gets a clean 404
|
||||||
|
// instead of an unretrievable upstream job.
|
||||||
|
asyncReq := httptest.NewRequest(http.MethodPost, "/v1/videos",
|
||||||
|
strings.NewReader(`{"model":"videogen-model","prompt":"a cat"}`))
|
||||||
|
asyncReq.Header.Set("Content-Type", "application/json")
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(w, asyncReq)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("POST /v1/videos: status=%d want 404 (async family not routed)", w.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user