From d297852b9e2a8f29cde298b0da9a913c72ef57fa Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Sun, 12 Jul 2026 09:53:33 -0400 Subject: [PATCH] fix: scope video dispatch to /v1/videos/sync + capture mask MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01AXQxVhXBw8PwFAtsVrXSmj --- internal/server/captures.go | 6 ++++- internal/server/server.go | 10 ++++--- internal/server/server_test.go | 48 +++++++++++++++++++++------------- 3 files changed, 42 insertions(+), 22 deletions(-) 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) } }