diff --git a/internal/server/captures.go b/internal/server/captures.go index fefab9a2..6dd56ba5 100644 --- a/internal/server/captures.go +++ b/internal/server/captures.go @@ -37,7 +37,8 @@ const ( ) // 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{ "/v1/audio/speech": captureReqAll | captureRespHeaders, "/v1/audio/voices": captureReqHeaders | captureRespAll, @@ -46,6 +47,9 @@ var captureFieldsByPath = map[string]captureFields{ "/v1/images/edits": captureReqHeaders | captureRespHeaders, "/sdapi/v1/txt2img": captureReqAll | 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 diff --git a/internal/server/server.go b/internal/server/server.go index 6bb5dded..cf70bb82 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -81,9 +81,13 @@ var modelPostFormRoutes = []string{ "/v1/audio/transcriptions", "/v1/images/edits", - // video generation (vLLM-Omni / OpenAI videos shape); extraction is - // content-type driven, so JSON bodies dispatch on these paths too - "/v1/videos", + // video generation: ONLY the blocking sync shape (vLLM-Omni + // /v1/videos/sync — the response body is the clip). The async + // 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", } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index c879c553..964fcb81 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -348,37 +348,49 @@ func TestServer_VideoRoutesDispatch(t *testing.T) { 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. var buf bytes.Buffer mw := multipart.NewWriter(&buf) mw.WriteField("model", "videogen-model") mw.WriteField("prompt", "a cat") 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()) - w = httptest.NewRecorder() + w := httptest.NewRecorder() s.ServeHTTP(w, req) if w.Code != http.StatusOK || w.Body.String() != "video response" { t.Fatalf("/v1/videos/sync: status=%d body=%q", w.Code, w.Body.String()) } - // Unknown model on a video route still 404s. - body = strings.NewReader(`{"model":"nope","prompt":"a cat"}`) - req = httptest.NewRequest(http.MethodPost, "/v1/videos", body) - req.Header.Set("Content-Type", "application/json") + // JSON bodies dispatch too (extraction is content-type driven). + jsonReq := httptest.NewRequest(http.MethodPost, "/v1/videos/sync", + strings.NewReader(`{"model":"videogen-model","prompt":"a cat"}`)) + jsonReq.Header.Set("Content-Type", "application/json") 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 { - 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) } }