package openai import ( "context" "io" "net/http" "net/http/httptest" "strconv" "strings" "sync" "testing" "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" apihandlers "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config" "github.com/tidwall/gjson" ) func performVideosEndpointRequest(t *testing.T, method string, endpointPath string, contentType string, body io.Reader, handler gin.HandlerFunc) *httptest.ResponseRecorder { t.Helper() gin.SetMode(gin.TestMode) router := gin.New() switch method { case http.MethodGet: router.GET(endpointPath, handler) default: router.POST(endpointPath, handler) } req := httptest.NewRequest(method, endpointPath, body) if contentType != "" { req.Header.Set("Content-Type", contentType) } resp := httptest.NewRecorder() router.ServeHTTP(resp, req) return resp } func performVideosRouteRequest(t *testing.T, method string, routePath string, requestPath string, contentType string, body io.Reader, handler gin.HandlerFunc) *httptest.ResponseRecorder { t.Helper() gin.SetMode(gin.TestMode) router := gin.New() switch method { case http.MethodGet: router.GET(routePath, handler) default: router.POST(routePath, handler) } req := httptest.NewRequest(method, requestPath, body) if contentType != "" { req.Header.Set("Content-Type", contentType) } resp := httptest.NewRecorder() router.ServeHTTP(resp, req) return resp } type videoAuthCaptureExecutor struct { mu sync.Mutex requestID string contentURL string authIDs []string models []string payloadModels []string } func (e *videoAuthCaptureExecutor) Identifier() string { return "xai" } func (e *videoAuthCaptureExecutor) Execute(_ context.Context, auth *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (coreexecutor.Response, error) { authID := "" if auth != nil { authID = auth.ID } e.mu.Lock() e.authIDs = append(e.authIDs, authID) e.models = append(e.models, req.Model) e.payloadModels = append(e.payloadModels, strings.TrimSpace(gjson.GetBytes(req.Payload, "model").String())) e.mu.Unlock() requestID := strings.TrimSpace(gjson.GetBytes(req.Payload, "request_id").String()) if requestID == "" { requestID = e.requestID } contentURL := strings.TrimSpace(e.contentURL) if contentURL == "" { contentURL = "https://vidgen.x.ai/video.mp4" } payload := []byte(`{"request_id":` + strconv.Quote(requestID) + `,"status":"completed","progress":100,"video":{"url":` + strconv.Quote(contentURL) + `,"duration":4}}`) return coreexecutor.Response{Payload: payload}, nil } func (e *videoAuthCaptureExecutor) ExecuteStream(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (*coreexecutor.StreamResult, error) { return nil, &coreauth.Error{Code: "not_implemented", Message: "ExecuteStream not implemented"} } func (e *videoAuthCaptureExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) { return auth, nil } func (e *videoAuthCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) { return coreexecutor.Response{}, &coreauth.Error{Code: "not_implemented", Message: "CountTokens not implemented"} } func (e *videoAuthCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) { return nil, &coreauth.Error{Code: "not_implemented", Message: "HttpRequest not implemented"} } func (e *videoAuthCaptureExecutor) AuthIDs() []string { e.mu.Lock() defer e.mu.Unlock() out := make([]string, len(e.authIDs)) copy(out, e.authIDs) return out } func (e *videoAuthCaptureExecutor) Models() []string { e.mu.Lock() defer e.mu.Unlock() out := make([]string, len(e.models)) copy(out, e.models) return out } func (e *videoAuthCaptureExecutor) PayloadModels() []string { e.mu.Lock() defer e.mu.Unlock() out := make([]string, len(e.payloadModels)) copy(out, e.payloadModels) return out } func resetVideoAuthBindingsForTest(t *testing.T) { t.Helper() previous := videoAuthBindings videoAuthBindings = newVideoAuthBindingStore() t.Cleanup(func() { videoAuthBindings = previous }) } func newVideoAuthBindingTestHandler(t *testing.T, executor *videoAuthCaptureExecutor) *OpenAIAPIHandler { t.Helper() manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) manager.RegisterExecutor(executor) authIDs := []string{executor.requestID + "-auth-a", executor.requestID + "-auth-b"} for _, authID := range authIDs { auth := &coreauth.Auth{ ID: authID, Provider: "xai", Status: coreauth.StatusActive, } if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { t.Fatalf("manager.Register(%s): %v", authID, errRegister) } registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: defaultXAIVideosModel}}) manager.RefreshSchedulerEntry(authID) } t.Cleanup(func() { for _, authID := range authIDs { registry.GetGlobalRegistry().UnregisterClient(authID) } }) base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager) return NewOpenAIAPIHandler(base) } func TestVideosModelValidationAllowsXAIVideoModel(t *testing.T) { for _, model := range []string{ "grok-imagine-video", "xai/grok-imagine-video", "x-ai/grok-imagine-video", "grok/grok-imagine-video", "grok-imagine-video-1.5", "xai/grok-imagine-video-1.5", "x-ai/grok-imagine-video-1.5", "grok/grok-imagine-video-1.5", "grok-imagine-video-1.5-preview", "xai/grok-imagine-video-1.5-preview", "x-ai/grok-imagine-video-1.5-preview", "grok/grok-imagine-video-1.5-preview", } { if !isSupportedVideosModel(model) { t.Fatalf("expected %s to be supported", model) } } if !isSupportedVideosModel("sora-2") { t.Fatal("expected sora-2 to be supported by the OpenAI video wrapper") } if isXAIVideosModel("sora-2") { t.Fatal("expected sora-2 not to be treated as a native xAI video model") } if isSupportedVideosModel("codex/grok-imagine-video") { t.Fatal("expected codex/grok-imagine-video to be rejected") } if isSupportedVideosModel("codex/grok-imagine-video-1.5") { t.Fatal("expected codex/grok-imagine-video-1.5 to be rejected") } if isSupportedVideosModel("codex/grok-imagine-video-1.5-preview") { t.Fatal("expected codex/grok-imagine-video-1.5-preview to be rejected") } } func TestBuildXAIVideosCreateRequestMapsSoraModelToXAIBackend(t *testing.T) { rawJSON := []byte(`{"model":"sora-2","prompt":"a cat playing piano","seconds":"8"}`) req, meta, err := buildXAIVideosCreateRequest(rawJSON, "sora-2") if err != nil { t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) } if got := gjson.GetBytes(req, "model").String(); got != defaultXAIVideosModel { t.Fatalf("upstream model = %q, want %s", got, defaultXAIVideosModel) } if meta.Model != defaultXAIVideosModel { t.Fatalf("response model = %q, want %s", meta.Model, defaultXAIVideosModel) } } func TestBuildXAIVideosCreateRequest(t *testing.T) { rawJSON := []byte(`{"model":"xai/grok-imagine-video","prompt":"a cat playing piano","seconds":"8","size":"1280x720","input_reference":{"image_url":"https://example.com/cat.png"}}`) req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video") if err != nil { t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) } if got := gjson.GetBytes(req, "model").String(); got != defaultXAIVideosModel { t.Fatalf("model = %q, want %s", got, defaultXAIVideosModel) } if got := gjson.GetBytes(req, "prompt").String(); got != "a cat playing piano" { t.Fatalf("prompt = %q", got) } if got := gjson.GetBytes(req, "duration").Int(); got != 8 { t.Fatalf("duration = %d, want 8", got) } if got := gjson.GetBytes(req, "aspect_ratio").String(); got != "16:9" { t.Fatalf("aspect_ratio = %q, want 16:9", got) } if got := gjson.GetBytes(req, "resolution").String(); got != "720p" { t.Fatalf("resolution = %q, want 720p", got) } if got := gjson.GetBytes(req, "image.url").String(); got != "https://example.com/cat.png" { t.Fatalf("image.url = %q", got) } if meta.Seconds != "8" || meta.Size != "1280x720" || meta.Prompt != "a cat playing piano" { t.Fatalf("unexpected meta: %+v", meta) } } func TestBuildXAIVideosCreateRequestAllowsVideo15Model(t *testing.T) { rawJSON := []byte(`{"model":"xai/grok-imagine-video-1.5","prompt":"a cat playing piano","seconds":"8"}`) req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video-1.5") if err != nil { t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) } if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15Model { t.Fatalf("model = %q, want %s", got, xaiVideos15Model) } if meta.Model != xaiVideos15Model { t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model) } if meta.RoutingModel != xaiVideos15Model { t.Fatalf("routing model = %q, want %s", meta.RoutingModel, xaiVideos15Model) } } func TestBuildXAIVideosCreateRequestNormalizesVideo15PreviewAlias(t *testing.T) { rawJSON := []byte(`{"model":"xai/grok-imagine-video-1.5-preview","prompt":"a cat playing piano","seconds":"8"}`) req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video-1.5-preview") if err != nil { t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) } if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15Model { t.Fatalf("model = %q, want %s", got, xaiVideos15Model) } if meta.Model != xaiVideos15Model { t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model) } if meta.RoutingModel != xaiVideos15PreviewAlias { t.Fatalf("routing model = %q, want %s", meta.RoutingModel, xaiVideos15PreviewAlias) } } func TestBuildXAIVideosCreateRequestAllowsCustomSeconds(t *testing.T) { rawJSON := []byte(`{"model":"grok-imagine-video","prompt":"a cat playing piano","seconds":"6"}`) req, meta, err := buildXAIVideosCreateRequest(rawJSON, "grok-imagine-video") if err != nil { t.Fatalf("buildXAIVideosCreateRequest() error = %v", err) } if got := gjson.GetBytes(req, "duration").Int(); got != 6 { t.Fatalf("duration = %d, want 6", got) } if meta.Seconds != "6" { t.Fatalf("meta seconds = %q, want 6", meta.Seconds) } } func TestBuildXAIVideosCreateRequestRejectsFileIDReference(t *testing.T) { rawJSON := []byte(`{"prompt":"animate","input_reference":{"file_id":"file_123"}}`) _, _, err := buildXAIVideosCreateRequest(rawJSON, defaultXAIVideosModel) if err == nil || !strings.Contains(err.Error(), "input_reference.file_id is not supported") { t.Fatalf("error = %v, want unsupported file_id error", err) } } func TestBuildVideosCreateAPIResponseFromXAI(t *testing.T) { meta := xaiVideoCreateMetadata{ Model: defaultXAIVideosModel, Prompt: "animate", Seconds: "4", Size: "720x1280", CreatedAt: 123, } out, err := buildVideosCreateAPIResponseFromXAI([]byte(`{"request_id":"vid_123"}`), meta) if err != nil { t.Fatalf("buildVideosCreateAPIResponseFromXAI() error = %v", err) } if got := gjson.GetBytes(out, "id").String(); got != "vid_123" { t.Fatalf("id = %q, want vid_123", got) } if got := gjson.GetBytes(out, "object").String(); got != "video" { t.Fatalf("object = %q, want video", got) } if got := gjson.GetBytes(out, "status").String(); got != "queued" { t.Fatalf("status = %q, want queued", got) } if got := gjson.GetBytes(out, "created_at").Int(); got != 123 { t.Fatalf("created_at = %d, want 123", got) } } func TestBuildVideosRetrieveAPIResponseFromXAI(t *testing.T) { payload := []byte(`{"object":"video","id":"91989464-273f-95df-8197-703b4fefd40e","model":"grok-imagine-video","status":"completed","progress":100,"seconds":"4","video":{"url":"https://vidgen.x.ai/xai-vidgen-bucket/xai-video-08609066-e7e9-43ba-bd8d-bd29cb6221d9.mp4","duration":4,"respect_moderation":true},"usage":{"cost_in_usd_ticks":2800000000}}`) out, err := buildVideosRetrieveAPIResponseFromXAI("91989464-273f-95df-8197-703b4fefd40e", payload, defaultOpenAIVideosModel) if err != nil { t.Fatalf("buildVideosRetrieveAPIResponseFromXAI() error = %v", err) } if got := gjson.GetBytes(out, "id").String(); got != "91989464-273f-95df-8197-703b4fefd40e" { t.Fatalf("id = %q", got) } if got := gjson.GetBytes(out, "object").String(); got != "video" { t.Fatalf("object = %q, want video", got) } if got := gjson.GetBytes(out, "model").String(); got != defaultXAIVideosModel { t.Fatalf("model = %q, want %s", got, defaultXAIVideosModel) } if got := gjson.GetBytes(out, "status").String(); got != "completed" { t.Fatalf("status = %q, want completed", got) } if got := gjson.GetBytes(out, "progress").Int(); got != 100 { t.Fatalf("progress = %d, want 100", got) } if got := gjson.GetBytes(out, "seconds").String(); got != "4" { t.Fatalf("seconds = %q, want 4", got) } if got := gjson.GetBytes(out, "video_url").String(); got != "https://vidgen.x.ai/xai-vidgen-bucket/xai-video-08609066-e7e9-43ba-bd8d-bd29cb6221d9.mp4" { t.Fatalf("video_url = %q", got) } if gjson.GetBytes(out, "video").Exists() { t.Fatalf("video field must not be exposed in OpenAI retrieve response: %s", string(out)) } if gjson.GetBytes(out, "usage").Exists() { t.Fatalf("usage field must not be exposed in OpenAI retrieve response: %s", string(out)) } } func TestBuildVideosRetrieveAPIResponseFromXAINormalizesTopLevelError(t *testing.T) { payload := []byte(`{"code":"invalid-argument","error":"1080p video resolution is not available for your team."}`) out, err := buildVideosRetrieveAPIResponseFromXAI("video_123", payload, defaultOpenAIVideosModel) if err != nil { t.Fatalf("buildVideosRetrieveAPIResponseFromXAI() error = %v", err) } if got := gjson.GetBytes(out, "status").String(); got != "failed" { t.Fatalf("status = %q, want failed", got) } if got := gjson.GetBytes(out, "progress").Int(); got != 0 { t.Fatalf("progress = %d, want 0", got) } if got := gjson.GetBytes(out, "error.code").String(); got != "invalid-argument" { t.Fatalf("error.code = %q, want invalid-argument", got) } if got := gjson.GetBytes(out, "error.message").String(); got != "1080p video resolution is not available for your team." { t.Fatalf("error.message = %q", got) } } func TestBuildVideosRetrieveAPIResponseFromXAINormalizesNestedError(t *testing.T) { payload := []byte(`{"status":"failed","error":{"message":"The request was rejected by the safety system.","type":"invalid_request_error","code":"content_policy_violation"}}`) out, err := buildVideosRetrieveAPIResponseFromXAI("video_123", payload, defaultOpenAIVideosModel) if err != nil { t.Fatalf("buildVideosRetrieveAPIResponseFromXAI() error = %v", err) } if got := gjson.GetBytes(out, "error.code").String(); got != "content_policy_violation" { t.Fatalf("error.code = %q, want content_policy_violation", got) } if got := gjson.GetBytes(out, "error.message").String(); got != "The request was rejected by the safety system." { t.Fatalf("error.message = %q", got) } if gjson.GetBytes(out, "error.type").Exists() { t.Fatalf("error.type must not be present: %s", string(out)) } } func TestXAIVideoContentURLFromPayload(t *testing.T) { payload := []byte(`{"status":"done","video":{"url":"https://vidgen.x.ai/video.mp4","duration":6}}`) got, err := xaiVideoContentURLFromPayload(payload) if err != nil { t.Fatalf("xaiVideoContentURLFromPayload() error = %v", err) } if got != "https://vidgen.x.ai/video.mp4" { t.Fatalf("url = %q, want https://vidgen.x.ai/video.mp4", got) } } func TestWriteVideoContentFromURL(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "video/mp4") w.Header().Set("Content-Disposition", `attachment; filename="video.mp4"`) _, _ = w.Write([]byte("video-bytes")) })) defer upstream.Close() gin.SetMode(gin.TestMode) resp := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(resp) ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_123/content", nil) base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, nil) handler := NewOpenAIAPIHandler(base) if err := handler.writeVideoContentFromURL(ctx, upstream.URL+"/video.mp4"); err != nil { t.Fatalf("writeVideoContentFromURL() error = %v", err) } if resp.Code != http.StatusOK { t.Fatalf("status = %d, want %d body=%s", resp.Code, http.StatusOK, resp.Body.String()) } if got := resp.Header().Get("Content-Type"); got != "video/mp4" { t.Fatalf("Content-Type = %q, want video/mp4", got) } if got := resp.Header().Get("Content-Disposition"); got != `attachment; filename="video.mp4"` { t.Fatalf("Content-Disposition = %q", got) } if got := resp.Body.String(); got != "video-bytes" { t.Fatalf("body = %q, want video-bytes", got) } } func TestWriteVideoContentFromURLUsesPinnedAuthProxy(t *testing.T) { resetVideoAuthBindingsForTest(t) upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "video/mp4") _, _ = w.Write([]byte("video-bytes")) })) defer upstream.Close() manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) authID := "video-content-auth" auth := &coreauth.Auth{ ID: authID, Provider: "xai", Status: coreauth.StatusActive, ProxyURL: "direct", } if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { t.Fatalf("manager.Register() error = %v", errRegister) } base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}, manager) handler := NewOpenAIAPIHandler(base) videoAuthBindings.set("video_123", authID, time.Hour) gin.SetMode(gin.TestMode) resp := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(resp) ctx.Params = gin.Params{{Key: "video_id", Value: "video_123"}} ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_123/content", nil) if err := handler.writeVideoContentFromURL(ctx, upstream.URL+"/video.mp4"); err != nil { t.Fatalf("writeVideoContentFromURL() error = %v", err) } client := handler.videoContentHTTPClient(ctx) transport, ok := client.Transport.(*http.Transport) if !ok { t.Fatalf("transport type = %T, want *http.Transport", client.Transport) } if transport.Proxy != nil { t.Fatal("expected pinned auth direct proxy to bypass global proxy") } if resp.Code != http.StatusOK { t.Fatalf("status = %d, want %d body=%s", resp.Code, http.StatusOK, resp.Body.String()) } } func TestWriteVideoContentFromURLFallsBackToGlobalProxy(t *testing.T) { resetVideoAuthBindingsForTest(t) base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}, nil) handler := NewOpenAIAPIHandler(base) gin.SetMode(gin.TestMode) resp := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(resp) ctx.Params = gin.Params{{Key: "video_id", Value: "video_456"}} ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_456/content", nil) client := handler.videoContentHTTPClient(ctx) transport, ok := client.Transport.(*http.Transport) if !ok { t.Fatalf("transport type = %T, want *http.Transport", client.Transport) } req, errRequest := http.NewRequest(http.MethodGet, "https://example.com/video.mp4", nil) if errRequest != nil { t.Fatalf("http.NewRequest() error = %v", errRequest) } proxyURL, errProxy := transport.Proxy(req) if errProxy != nil { t.Fatalf("transport.Proxy() error = %v", errProxy) } if proxyURL == nil || proxyURL.String() != "http://global-proxy.example.com:8080" { t.Fatalf("proxy URL = %v, want http://global-proxy.example.com:8080", proxyURL) } } func TestVideosContentUsesSelectedAuthProxyForDownload(t *testing.T) { resetVideoAuthBindingsForTest(t) upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "video/mp4") _, _ = w.Write([]byte("video-bytes")) })) defer upstream.Close() var proxyMu sync.Mutex proxyHits := 0 globalProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { proxyMu.Lock() proxyHits++ proxyMu.Unlock() http.Error(w, "unexpected proxy", http.StatusBadGateway) })) defer globalProxy.Close() videoID := "video-content-selected" authID := "video-content-selected-auth" executor := &videoAuthCaptureExecutor{ requestID: videoID, contentURL: upstream.URL + "/video.mp4", } manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) manager.RegisterExecutor(executor) auth := &coreauth.Auth{ ID: authID, Provider: "xai", Status: coreauth.StatusActive, ProxyURL: "direct", } if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { t.Fatalf("manager.Register() error = %v", errRegister) } registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: defaultXAIVideosModel}}) t.Cleanup(func() { registry.GetGlobalRegistry().UnregisterClient(authID) }) base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: globalProxy.URL}, manager) handler := NewOpenAIAPIHandler(base) resp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id/content", openAIVideosPath+"/"+videoID+"/content", "", nil, handler.VideosContent) if resp.Code != http.StatusOK { t.Fatalf("content status = %d, want %d: %s", resp.Code, http.StatusOK, resp.Body.String()) } if got := resp.Body.String(); got != "video-bytes" { t.Fatalf("content body = %q, want video-bytes", got) } authIDs := executor.AuthIDs() if len(authIDs) != 1 || authIDs[0] != authID { t.Fatalf("authIDs = %v, want [%s]", authIDs, authID) } if boundAuthID, ok := videoAuthBindings.get(videoID); !ok || boundAuthID != authID { t.Fatalf("bound auth = %q ok=%v, want %s", boundAuthID, ok, authID) } proxyMu.Lock() gotProxyHits := proxyHits proxyMu.Unlock() if gotProxyHits != 0 { t.Fatalf("global proxy hits = %d, want 0", gotProxyHits) } } func TestVideosCreateRejectsUnsupportedModel(t *testing.T) { handler := &OpenAIAPIHandler{} body := strings.NewReader(`{"model":"not-a-video-model","prompt":"make a video"}`) resp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", body, handler.VideosCreate) if resp.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String()) } if got := gjson.GetBytes(resp.Body.Bytes(), "object").String(); got != "video" { t.Fatalf("object = %q, want video", got) } if got := gjson.GetBytes(resp.Body.Bytes(), "model").String(); got != "not-a-video-model" { t.Fatalf("model = %q, want not-a-video-model", got) } if got := gjson.GetBytes(resp.Body.Bytes(), "status").String(); got != "failed" { t.Fatalf("status = %q, want failed", got) } if got := gjson.GetBytes(resp.Body.Bytes(), "progress").Int(); got != 0 { t.Fatalf("progress = %d, want 0", got) } if got := gjson.GetBytes(resp.Body.Bytes(), "error.code").String(); got != "invalid_request_error" { t.Fatalf("error.code = %q, want invalid_request_error", got) } expectedMessage := "Model not-a-video-model is not supported on " + openAIVideosPath + ". Use " + defaultOpenAIVideosModel + "." if got := gjson.GetBytes(resp.Body.Bytes(), "error.message").String(); got != expectedMessage { t.Fatalf("error.message = %q, want %q", got, expectedMessage) } if gjson.GetBytes(resp.Body.Bytes(), "error.type").Exists() { t.Fatalf("error.type must not be present: %s", resp.Body.String()) } if id := gjson.GetBytes(resp.Body.Bytes(), "id").String(); !strings.HasPrefix(id, "video_") { t.Fatalf("id = %q, want video_ prefix", id) } } func TestVideosCreateInvalidSizeReturnsFailedVideoResource(t *testing.T) { handler := &OpenAIAPIHandler{} body := strings.NewReader(`{"model":"sora-2","prompt":"make a video","size":"1080x1920"}`) resp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", body, handler.VideosCreate) if resp.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String()) } if got := gjson.GetBytes(resp.Body.Bytes(), "object").String(); got != "video" { t.Fatalf("object = %q, want video", got) } if got := gjson.GetBytes(resp.Body.Bytes(), "model").String(); got != defaultXAIVideosModel { t.Fatalf("model = %q, want %s", got, defaultXAIVideosModel) } if got := gjson.GetBytes(resp.Body.Bytes(), "status").String(); got != "failed" { t.Fatalf("status = %q, want failed", got) } if got := gjson.GetBytes(resp.Body.Bytes(), "progress").Int(); got != 0 { t.Fatalf("progress = %d, want 0", got) } if got := gjson.GetBytes(resp.Body.Bytes(), "error.code").String(); got != "invalid_request_error" { t.Fatalf("error.code = %q, want invalid_request_error", got) } expectedMessage := "Invalid request: size must be one of 720x1280, 1280x720, 1024x1792, or 1792x1024" if got := gjson.GetBytes(resp.Body.Bytes(), "error.message").String(); got != expectedMessage { t.Fatalf("error.message = %q, want %q", got, expectedMessage) } if gjson.GetBytes(resp.Body.Bytes(), "error.type").Exists() { t.Fatalf("error.type must not be present: %s", resp.Body.String()) } } func TestXAIVideosNativeRejectsUnsupportedModel(t *testing.T) { handler := &OpenAIAPIHandler{} body := strings.NewReader(`{"model":"sora-2","prompt":"make a video"}`) resp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", body, handler.XAIVideosGenerations) if resp.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String()) } message := gjson.GetBytes(resp.Body.Bytes(), "error.message").String() expectedMessage := "Model sora-2 is not supported on " + xaiVideosGenerationsAPI + ", " + xaiVideosEditsAPI + ", or " + xaiVideosExtensionsAPI + ". Use " + defaultXAIVideosModel + "." if message != expectedMessage { t.Fatalf("error message = %q, want %q", message, expectedMessage) } } func TestXAIVideosNativeRejectsInvalidJSON(t *testing.T) { handler := &OpenAIAPIHandler{} body := strings.NewReader(`{"model":`) resp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosEditsAPI, "application/json", body, handler.XAIVideosEdits) if resp.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String()) } if got := gjson.GetBytes(resp.Body.Bytes(), "error.type").String(); got != "invalid_request_error" { t.Fatalf("error type = %q, want invalid_request_error", got) } } func TestVideosCreateBindsRetrieveToSelectedAuth(t *testing.T) { resetVideoAuthBindingsForTest(t) executor := &videoAuthCaptureExecutor{requestID: "video-openai-bound"} handler := newVideoAuthBindingTestHandler(t, executor) createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"sora-2","prompt":"make a video"}`), handler.VideosCreate) if createResp.Code != http.StatusOK { t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) } videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String() if videoID != executor.requestID { t.Fatalf("created video id = %q, want %q", videoID, executor.requestID) } if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != defaultXAIVideosModel { t.Fatalf("created model = %q, want %s", got, defaultXAIVideosModel) } retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve) if retrieveResp.Code != http.StatusOK { t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) } authIDs := executor.AuthIDs() if len(authIDs) != 2 { t.Fatalf("authIDs = %v, want two calls", authIDs) } if authIDs[1] != authIDs[0] { t.Fatalf("retrieve auth = %q, want create auth %q; sequence=%v", authIDs[1], authIDs[0], authIDs) } } func TestXAIVideosNativeCreateBindsRetrieveToSelectedAuth(t *testing.T) { resetVideoAuthBindingsForTest(t) executor := &videoAuthCaptureExecutor{requestID: "video-xai-bound"} handler := newVideoAuthBindingTestHandler(t, executor) createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video","prompt":"make a video"}`), handler.XAIVideosGenerations) if createResp.Code != http.StatusOK { t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) } videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String() if videoID != executor.requestID { t.Fatalf("created request_id = %q, want %q", videoID, executor.requestID) } retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve) if retrieveResp.Code != http.StatusOK { t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) } authIDs := executor.AuthIDs() if len(authIDs) != 2 { t.Fatalf("authIDs = %v, want two calls", authIDs) } if authIDs[1] != authIDs[0] { t.Fatalf("retrieve auth = %q, want create auth %q; sequence=%v", authIDs[1], authIDs[0], authIDs) } } func TestXAIVideosNativeRetrieveUsesCanonicalBoundModel(t *testing.T) { resetVideoAuthBindingsForTest(t) executor := &videoAuthCaptureExecutor{requestID: "video-xai-1.5-bound"} manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) manager.RegisterExecutor(executor) authModels := []struct { authID string model string }{ {authID: "video-xai-1.5-default-auth", model: defaultXAIVideosModel}, {authID: "video-xai-1.5-auth", model: xaiVideos15Model}, } for _, entry := range authModels { auth := &coreauth.Auth{ ID: entry.authID, Provider: "xai", Status: coreauth.StatusActive, } if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { t.Fatalf("manager.Register(%s): %v", entry.authID, errRegister) } registry.GetGlobalRegistry().RegisterClient(entry.authID, auth.Provider, []*registry.ModelInfo{{ID: entry.model}}) manager.RefreshSchedulerEntry(entry.authID) } t.Cleanup(func() { for _, entry := range authModels { registry.GetGlobalRegistry().UnregisterClient(entry.authID) } }) base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager) handler := NewOpenAIAPIHandler(base) createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5","prompt":"make a video"}`), handler.XAIVideosGenerations) if createResp.Code != http.StatusOK { t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) } videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String() if videoID != executor.requestID { t.Fatalf("created request_id = %q, want %q", videoID, executor.requestID) } retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve) if retrieveResp.Code != http.StatusOK { t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) } authIDs := executor.AuthIDs() if len(authIDs) != 2 { t.Fatalf("authIDs = %v, want two calls", authIDs) } if authIDs[0] != "video-xai-1.5-auth" || authIDs[1] != authIDs[0] { t.Fatalf("authIDs = %v, want both calls to use video-xai-1.5-auth", authIDs) } models := executor.Models() if len(models) != 2 { t.Fatalf("models = %v, want two calls", models) } if models[0] != xaiVideos15Model || models[1] != xaiVideos15Model { t.Fatalf("models = %v, want both calls to use %s", models, xaiVideos15Model) } payloadModels := executor.PayloadModels() if len(payloadModels) != 2 || payloadModels[0] != xaiVideos15Model { t.Fatalf("payload models = %v, want create payload model %s", payloadModels, xaiVideos15Model) } binding, ok := videoAuthBindings.getBinding(videoID) if !ok { t.Fatal("video auth binding was not stored") } if binding.authID != "video-xai-1.5-auth" || binding.model != xaiVideos15Model { t.Fatalf("binding = {authID:%q model:%q}, want {authID:%q model:%q}", binding.authID, binding.model, "video-xai-1.5-auth", xaiVideos15Model) } } func TestVideosCreatePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) { resetVideoAuthBindingsForTest(t) executor := &videoAuthCaptureExecutor{requestID: "video-openai-preview-alias"} handler := newVideoSingleModelAuthTestHandler(t, executor, "video-openai-preview-auth", xaiVideos15PreviewAlias) createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.VideosCreate) if createResp.Code != http.StatusOK { t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) } videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String() if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != xaiVideos15Model { t.Fatalf("response model = %q, want %s", got, xaiVideos15Model) } retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve) if retrieveResp.Code != http.StatusOK { t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) } assertPreviewAliasRouting(t, executor, videoID, "video-openai-preview-auth") } func TestVideosCreatePreviewAliasUsesDefaultXAIModelsWithGAPayload(t *testing.T) { resetVideoAuthBindingsForTest(t) executor := &videoAuthCaptureExecutor{requestID: "video-openai-preview-default-models"} handler := newVideoAuthTestHandler(t, executor, "video-openai-preview-default-auth", registry.GetXAIModels()) createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.VideosCreate) if createResp.Code != http.StatusOK { t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) } videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String() if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != xaiVideos15Model { t.Fatalf("response model = %q, want %s", got, xaiVideos15Model) } retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve) if retrieveResp.Code != http.StatusOK { t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) } assertPreviewAliasRouting(t, executor, videoID, "video-openai-preview-default-auth") } func TestXAIVideosNativePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) { resetVideoAuthBindingsForTest(t) executor := &videoAuthCaptureExecutor{requestID: "video-native-preview-alias"} handler := newVideoSingleModelAuthTestHandler(t, executor, "video-native-preview-auth", xaiVideos15PreviewAlias) createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.XAIVideosGenerations) if createResp.Code != http.StatusOK { t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String()) } videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String() retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve) if retrieveResp.Code != http.StatusOK { t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String()) } assertPreviewAliasRouting(t, executor, videoID, "video-native-preview-auth") } func newVideoSingleModelAuthTestHandler(t *testing.T, executor *videoAuthCaptureExecutor, authID string, model string) *OpenAIAPIHandler { t.Helper() return newVideoAuthTestHandler(t, executor, authID, []*registry.ModelInfo{{ID: model}}) } func newVideoAuthTestHandler(t *testing.T, executor *videoAuthCaptureExecutor, authID string, models []*registry.ModelInfo) *OpenAIAPIHandler { t.Helper() manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil) manager.RegisterExecutor(executor) auth := &coreauth.Auth{ ID: authID, Provider: "xai", Status: coreauth.StatusActive, } if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { t.Fatalf("manager.Register(%s): %v", authID, errRegister) } registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, models) manager.RefreshSchedulerEntry(authID) t.Cleanup(func() { registry.GetGlobalRegistry().UnregisterClient(authID) }) base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager) return NewOpenAIAPIHandler(base) } func assertPreviewAliasRouting(t *testing.T, executor *videoAuthCaptureExecutor, videoID string, authID string) { t.Helper() authIDs := executor.AuthIDs() if len(authIDs) != 2 || authIDs[0] != authID || authIDs[1] != authID { t.Fatalf("authIDs = %v, want both calls to use %s", authIDs, authID) } models := executor.Models() if len(models) != 2 || models[0] != xaiVideos15PreviewAlias || models[1] != xaiVideos15PreviewAlias { t.Fatalf("models = %v, want both calls to route with %s", models, xaiVideos15PreviewAlias) } payloadModels := executor.PayloadModels() if len(payloadModels) != 2 || payloadModels[0] != xaiVideos15Model { t.Fatalf("payload models = %v, want create payload model %s", payloadModels, xaiVideos15Model) } binding, ok := videoAuthBindings.getBinding(videoID) if !ok { t.Fatal("video auth binding was not stored") } if binding.authID != authID || binding.model != xaiVideos15PreviewAlias { t.Fatalf("binding = {authID:%q model:%q}, want {authID:%q model:%q}", binding.authID, binding.model, authID, xaiVideos15PreviewAlias) } } func TestVideoAuthBindingTTLUsesConfig(t *testing.T) { base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{VideoResultAuthCacheTTL: "45m"}, nil) handler := NewOpenAIAPIHandler(base) if got := handler.videoAuthBindingTTL(); got != 45*time.Minute { t.Fatalf("videoAuthBindingTTL() = %v, want 45m", got) } base = apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{VideoResultAuthCacheTTL: "invalid"}, nil) handler = NewOpenAIAPIHandler(base) if got := handler.videoAuthBindingTTL(); got != defaultVideoAuthBindingTTL { t.Fatalf("invalid videoAuthBindingTTL() = %v, want %v", got, defaultVideoAuthBindingTTL) } } func TestVideoAuthBindingStoreExpiresEntries(t *testing.T) { store := newVideoAuthBindingStore() store.entries["video-expired"] = videoAuthBinding{ authID: "auth-expired", expiresAt: time.Now().Add(-time.Second), } if authID, ok := store.get("video-expired"); ok { t.Fatalf("expired binding returned authID=%q", authID) } if _, exists := store.entries["video-expired"]; exists { t.Fatal("expired binding was not removed") } } func TestVideosCreateFormRequest(t *testing.T) { rawJSON, err := videosCreateRequestFromFormContext("model=grok-imagine-video&prompt=make+a+video&seconds=4&size=720x1280&input_reference%5Bimage_url%5D=https%3A%2F%2Fexample.com%2Fa.png") if err != nil { t.Fatalf("videosCreateRequestFromFormContext() error = %v", err) } if got := gjson.GetBytes(rawJSON, "input_reference.image_url").String(); got != "https://example.com/a.png" { t.Fatalf("input_reference.image_url = %q", got) } } func videosCreateRequestFromFormContext(body string) ([]byte, error) { gin.SetMode(gin.TestMode) router := gin.New() var rawJSON []byte var err error router.POST(videosPath, func(c *gin.Context) { rawJSON, err = videosCreateRequestFromForm(c) }) req := httptest.NewRequest(http.MethodPost, videosPath, strings.NewReader(body)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := httptest.NewRecorder() router.ServeHTTP(resp, req) return rawJSON, err }