From cecb89880e6a8200641de299aa79ab694fb778cd Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Sun, 12 Jul 2026 09:25:55 -0400 Subject: [PATCH 1/2] feat(server): dispatch /v1/videos and /v1/videos/sync by model Registers the OpenAI/vLLM-Omni video generation endpoints as model-dispatched routes so llama-swap can swap in video upstreams (e.g. vllm serve --omni, or a ComfyUI shim exposing the same shape). Extraction is content-type driven, so both multipart form and JSON bodies resolve the model field. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AXQxVhXBw8PwFAtsVrXSmj --- internal/server/server.go | 5 ++++ internal/server/server_test.go | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/internal/server/server.go b/internal/server/server.go index 0d27d89d..6bb5dded 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -80,6 +80,11 @@ var modelPostJSONRoutes = []string{ 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", + "/v1/videos/sync", } // modelGetRoutes are model-dispatched GET endpoints (the model arrives as a diff --git a/internal/server/server_test.go b/internal/server/server_test.go index f16a915f..c879c553 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -1,9 +1,11 @@ package server import ( + "bytes" "context" "encoding/json" "io" + "mime/multipart" "net/http" "net/http/httptest" "strings" @@ -339,3 +341,44 @@ func TestServer_LogStream_UnknownID_Returns400(t *testing.T) { t.Errorf("status=%d want 400", w.Code) } } + +func TestServer_VideoRoutesDispatch(t *testing.T) { + s := newTestServer( + newStubRouter([]string{"videogen-model"}, "video response"), + 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.Header.Set("Content-Type", mw.FormDataContentType()) + 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") + w = httptest.NewRecorder() + s.ServeHTTP(w, req) + if w.Code != http.StatusNotFound { + t.Fatalf("/v1/videos unknown model: status=%d want 404", w.Code) + } +} -- 2.52.0 From d297852b9e2a8f29cde298b0da9a913c72ef57fa Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Sun, 12 Jul 2026 09:53:33 -0400 Subject: [PATCH 2/2] 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) } } -- 2.52.0